From 3c9a51532bf03ac3e67f752ccb2598d762e48bc0 Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Thu, 18 Jun 2026 02:19:26 +0800 Subject: [PATCH 01/39] feat(banked-reset): initial commit --- app/core/clients/headers.py | 29 ++ app/core/clients/rate_limit_reset_credits.py | 279 ++++++++++++++ app/core/clients/usage.py | 9 +- app/core/config/settings.py | 2 + .../usage/reset_credits_refresh_scheduler.py | 141 +++++++ app/main.py | 6 + app/modules/accounts/mappers.py | 11 + app/modules/accounts/schemas.py | 6 + .../rate_limit_reset_credits/__init__.py | 0 app/modules/rate_limit_reset_credits/api.py | 137 +++++++ app/modules/rate_limit_reset_credits/store.py | 40 ++ frontend/src/components/confirm-dialog.tsx | 12 +- frontend/src/features/accounts/api.ts | 16 + .../components/account-actions.test.tsx | 61 +++ .../accounts/components/account-actions.tsx | 35 ++ .../components/account-detail.test.tsx | 2 + .../accounts/components/account-detail.tsx | 3 + .../components/account-list-item.test.tsx | 24 ++ .../accounts/components/account-list-item.tsx | 9 +- .../accounts/components/accounts-page.tsx | 11 + .../reset-credit-confirm-dialog.test.tsx | 142 +++++++ .../reset-credit-confirm-dialog.tsx | 116 ++++++ .../features/accounts/hooks/use-accounts.ts | 35 +- frontend/src/features/accounts/schemas.ts | 33 ++ .../src/features/accounts/sorting.test.ts | 99 +++++ frontend/src/features/accounts/sorting.ts | 34 +- .../components/account-card.test.tsx | 19 + .../dashboard/components/account-card.tsx | 38 +- .../components/account-list.test.tsx | 27 +- .../dashboard/components/account-list.tsx | 26 +- .../dashboard/components/dashboard-page.tsx | 16 +- frontend/src/utils/formatters.test.ts | 24 ++ frontend/src/utils/formatters.ts | 29 ++ .../.openspec.yaml | 2 + .../add-rate-limit-reset-credits/design.md | 68 ++++ .../add-rate-limit-reset-credits/proposal.md | 30 ++ .../specs/frontend-architecture/spec.md | 89 +++++ .../specs/rate-limit-reset-credits/context.md | 110 ++++++ .../specs/rate-limit-reset-credits/spec.md | 87 +++++ .../add-rate-limit-reset-credits/tasks.md | 50 +++ tests/conftest.py | 1 + .../unit/test_rate_limit_reset_credits_api.py | 247 ++++++++++++ .../test_rate_limit_reset_credits_client.py | 361 ++++++++++++++++++ .../test_rate_limit_reset_credits_mapper.py | 92 +++++ ...test_rate_limit_reset_credits_scheduler.py | 272 +++++++++++++ .../test_rate_limit_reset_credits_store.py | 101 +++++ 46 files changed, 2963 insertions(+), 18 deletions(-) create mode 100644 app/core/clients/headers.py create mode 100644 app/core/clients/rate_limit_reset_credits.py create mode 100644 app/core/usage/reset_credits_refresh_scheduler.py create mode 100644 app/modules/rate_limit_reset_credits/__init__.py create mode 100644 app/modules/rate_limit_reset_credits/api.py create mode 100644 app/modules/rate_limit_reset_credits/store.py create mode 100644 frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx create mode 100644 frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx create mode 100644 frontend/src/features/accounts/sorting.test.ts create mode 100644 openspec/changes/add-rate-limit-reset-credits/.openspec.yaml create mode 100644 openspec/changes/add-rate-limit-reset-credits/design.md create mode 100644 openspec/changes/add-rate-limit-reset-credits/proposal.md create mode 100644 openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md create mode 100644 openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md create mode 100644 openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md create mode 100644 openspec/changes/add-rate-limit-reset-credits/tasks.md create mode 100644 tests/unit/test_rate_limit_reset_credits_api.py create mode 100644 tests/unit/test_rate_limit_reset_credits_client.py create mode 100644 tests/unit/test_rate_limit_reset_credits_mapper.py create mode 100644 tests/unit/test_rate_limit_reset_credits_scheduler.py create mode 100644 tests/unit/test_rate_limit_reset_credits_store.py diff --git a/app/core/clients/headers.py b/app/core/clients/headers.py new file mode 100644 index 000000000..bc2758978 --- /dev/null +++ b/app/core/clients/headers.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from app.core.utils.request_id import get_request_id + + +def build_chatgpt_auth_headers( + access_token: str, + account_id: str | None, + *, + extra: dict[str, str] | None = None, +) -> dict[str, str]: + """Build the headers required to call ChatGPT ``backend-api`` endpoints. + + Includes the OAuth bearer token and the ``chatgpt-account-id`` header. The + account-id header is omitted when the id is a synthetic ``email_``/``local_`` + prefix, matching upstream behavior. An active request id (if any) is attached. + """ + headers: dict[str, str] = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/json", + } + request_id = get_request_id() + if request_id: + headers["x-request-id"] = request_id + if account_id and not account_id.startswith(("email_", "local_")): + headers["chatgpt-account-id"] = account_id + if extra: + headers.update(extra) + return headers diff --git a/app/core/clients/rate_limit_reset_credits.py b/app/core/clients/rate_limit_reset_credits.py new file mode 100644 index 000000000..4c40d9082 --- /dev/null +++ b/app/core/clients/rate_limit_reset_credits.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import asyncio +import logging +import uuid +from datetime import datetime +from typing import cast + +import aiohttp +from aiohttp_retry import ExponentialRetry, RetryClient +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +from app.core.clients.headers import build_chatgpt_auth_headers +from app.core.clients.http import lease_retry_client +from app.core.config.settings import get_settings +from app.core.types import JsonObject +from app.core.utils.request_id import get_request_id + +RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504} +RETRY_START_TIMEOUT = 0.5 +RETRY_MAX_TIMEOUT = 2.0 + +logger = logging.getLogger(__name__) + + +class ResetCreditFetchError(Exception): + def __init__(self, status_code: int, message: str, code: str | None = None) -> None: + super().__init__(message) + self.status_code = status_code + self.message = message + self.code = code + + +class ConsumeResetCreditError(Exception): + def __init__(self, status_code: int, message: str, code: str | None = None) -> None: + super().__init__(message) + self.status_code = status_code + self.message = message + self.code = code + + +class ResetCreditItem(BaseModel): + model_config = ConfigDict(extra="ignore") + + id: str + reset_type: str | None = None + status: str | None = None + granted_at: datetime | None = None + expires_at: datetime | None = None + title: str | None = None + description: str | None = None + redeem_started_at: datetime | None = None + redeemed_at: datetime | None = None + + +class ResetCreditsResponse(BaseModel): + model_config = ConfigDict(extra="ignore") + + credits: list[ResetCreditItem] = Field(default_factory=list) + available_count: int = 0 + + +class ConsumeResetCreditCredit(BaseModel): + model_config = ConfigDict(extra="ignore") + + id: str | None = None + reset_type: str | None = None + status: str | None = None + redeemed_at: datetime | None = None + + +class ConsumeResetCreditResponse(BaseModel): + model_config = ConfigDict(extra="ignore") + + code: str | None = None + credit: ConsumeResetCreditCredit | None = None + windows_reset: int | None = None + + +class RateLimitResetCreditsSnapshot(BaseModel): + """In-memory snapshot of a single account's banked reset credits. + + Carries the upstream ``available_count``, the soonest expiry among the + available credits, and the full credit list so dashboard endpoints can + render details and the consume path can re-select at click time. + """ + + model_config = ConfigDict(extra="ignore") + + available_count: int = 0 + nearest_expires_at: datetime | None = None + credits: list[ResetCreditItem] = Field(default_factory=list) + + +async def fetch_reset_credits( + access_token: str, + account_id: str | None, + *, + base_url: str | None = None, + timeout_seconds: float | None = None, + max_retries: int | None = None, + client: RetryClient | None = None, +) -> ResetCreditsResponse: + settings = get_settings() + usage_base = base_url or settings.upstream_base_url + url = _reset_credits_url(usage_base) + timeout = aiohttp.ClientTimeout(total=timeout_seconds or settings.usage_fetch_timeout_seconds) + retries = max_retries if max_retries is not None else settings.usage_fetch_max_retries + headers = build_chatgpt_auth_headers(access_token, account_id) + retry_options = _retry_options(retries + 1) + + try: + async with lease_retry_client(client) as retry_client: + async with retry_client.request( + "GET", + url, + headers=headers, + timeout=timeout, + retry_options=retry_options, + ) as resp: + data = await _safe_json(resp) + if resp.status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits fetch failed ({resp.status})" + logger.warning( + "Reset credits fetch failed request_id=%s status=%s code=%s message=%s", + get_request_id(), + resp.status, + code, + message, + ) + raise ResetCreditFetchError(resp.status, message, code=code) + try: + return ResetCreditsResponse.model_validate(data) + except ValidationError as exc: + logger.warning("Reset credits fetch invalid payload request_id=%s", get_request_id()) + raise ResetCreditFetchError(502, "Invalid reset credits payload") from exc + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + logger.warning("Reset credits fetch error request_id=%s error=%s", get_request_id(), exc) + raise ResetCreditFetchError(0, f"Reset credits fetch failed: {exc}") from exc + + +async def consume_reset_credit( + access_token: str, + account_id: str | None, + credit_id: str, + *, + base_url: str | None = None, + timeout_seconds: float | None = None, + max_retries: int | None = None, + client: RetryClient | None = None, +) -> ConsumeResetCreditResponse: + settings = get_settings() + usage_base = base_url or settings.upstream_base_url + url = _consume_url(usage_base) + timeout = aiohttp.ClientTimeout(total=timeout_seconds or settings.usage_fetch_timeout_seconds) + retries = max_retries if max_retries is not None else settings.usage_fetch_max_retries + headers = build_chatgpt_auth_headers( + access_token, + account_id, + extra={"Content-Type": "application/json"}, + ) + redeem_request_id = str(uuid.uuid4()) + body = {"credit_id": credit_id, "redeem_request_id": redeem_request_id} + retry_options = _retry_options(retries + 1) + + try: + async with lease_retry_client(client) as retry_client: + async with retry_client.request( + "POST", + url, + headers=headers, + json=body, + timeout=timeout, + retry_options=retry_options, + ) as resp: + data = await _safe_json(resp) + if resp.status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits consume failed ({resp.status})" + logger.warning( + "Reset credits consume failed request_id=%s status=%s code=%s message=%s", + get_request_id(), + resp.status, + code, + message, + ) + raise ConsumeResetCreditError(resp.status, message, code=code) + try: + return ConsumeResetCreditResponse.model_validate(data) + except ValidationError as exc: + logger.warning("Reset credits consume invalid payload request_id=%s", get_request_id()) + raise ConsumeResetCreditError(502, "Invalid reset credits consume payload") from exc + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + logger.warning("Reset credits consume error request_id=%s error=%s", get_request_id(), exc) + raise ConsumeResetCreditError(0, f"Reset credits consume failed: {exc}") from exc + + +def build_snapshot(response: ResetCreditsResponse) -> RateLimitResetCreditsSnapshot: + """Project an upstream list response into the cached snapshot shape.""" + nearest = _nearest_available_expires_at(response.credits) + return RateLimitResetCreditsSnapshot( + available_count=response.available_count, + nearest_expires_at=nearest, + credits=list(response.credits), + ) + + +def _nearest_available_expires_at(credits: list[ResetCreditItem]) -> datetime | None: + candidates = [ + credit.expires_at for credit in credits if credit.status == "available" and credit.expires_at is not None + ] + return min(candidates) if candidates else None + + +def _reset_credits_url(base_url: str) -> str: + normalized = base_url.rstrip("/") + if "/backend-api" not in normalized: + normalized = f"{normalized}/backend-api" + return f"{normalized}/wham/rate-limit-reset-credits" + + +def _consume_url(base_url: str) -> str: + return f"{_reset_credits_url(base_url)}/consume" + + +async def _safe_json(resp: aiohttp.ClientResponse) -> JsonObject: + try: + data = await resp.json(content_type=None) + except Exception: + text = await resp.text() + return {"error": {"message": text.strip()}} + return data if isinstance(data, dict) else cast(JsonObject, {"error": {"message": str(data)}}) + + +class _ErrorEnvelope(BaseModel): + model_config = ConfigDict(extra="ignore") + + error: dict[str, str | None] | str | None = None + error_description: str | None = None + message: str | None = None + + +def _extract_error_message(payload: JsonObject) -> str | None: + envelope = _ErrorEnvelope.model_validate(payload) + error = envelope.error + if isinstance(error, dict): + message = error.get("message") + if isinstance(message, str) and message: + return message + description = error.get("error_description") + if isinstance(description, str) and description: + return description + if isinstance(error, str) and error: + return envelope.error_description or error + return envelope.message + + +def _extract_error_code(payload: JsonObject) -> str | None: + envelope = _ErrorEnvelope.model_validate(payload) + error = envelope.error + if isinstance(error, dict): + code = error.get("code") + if isinstance(code, str): + normalized = code.strip().lower() + return normalized or None + return None + + +def _retry_options(attempts: int) -> ExponentialRetry: + return ExponentialRetry( + attempts=attempts, + start_timeout=RETRY_START_TIMEOUT, + max_timeout=RETRY_MAX_TIMEOUT, + factor=2.0, + statuses=RETRYABLE_STATUS, + exceptions={aiohttp.ClientError, asyncio.TimeoutError}, + retry_all_server_errors=False, + ) diff --git a/app/core/clients/usage.py b/app/core/clients/usage.py index 0a8d7e461..ae8342b4e 100644 --- a/app/core/clients/usage.py +++ b/app/core/clients/usage.py @@ -13,6 +13,7 @@ create_codex_session, require_route_or_direct_egress_opt_in, ) +from app.core.clients.headers import build_chatgpt_auth_headers from app.core.clients.http import lease_retry_client from app.core.config.settings import get_settings from app.core.types import JsonObject @@ -220,13 +221,7 @@ def _usage_url(base_url: str) -> str: def _usage_headers(access_token: str, account_id: str | None) -> dict[str, str]: - headers = {"Authorization": f"Bearer {access_token}", "Accept": "application/json"} - request_id = get_request_id() - if request_id: - headers["x-request-id"] = request_id - if account_id and not account_id.startswith(("email_", "local_")): - headers["chatgpt-account-id"] = account_id - return headers + return build_chatgpt_auth_headers(access_token, account_id) async def _safe_json(resp: aiohttp.ClientResponse) -> JsonObject: diff --git a/app/core/config/settings.py b/app/core/config/settings.py index 2c286a69d..3c854f783 100644 --- a/app/core/config/settings.py +++ b/app/core/config/settings.py @@ -194,6 +194,8 @@ class Settings(BaseSettings): usage_fetch_max_retries: int = 2 usage_refresh_enabled: bool = True usage_refresh_interval_seconds: int = Field(default=60, gt=0) + rate_limit_reset_credits_refresh_enabled: bool = True + rate_limit_reset_credits_refresh_interval_seconds: int = Field(default=60, gt=0) openai_cache_affinity_max_age_seconds: int = Field(default=1800, gt=0) warmup_model: str = "gpt-5.4-mini" openai_prompt_cache_key_derivation_enabled: bool = True diff --git a/app/core/usage/reset_credits_refresh_scheduler.py b/app/core/usage/reset_credits_refresh_scheduler.py new file mode 100644 index 000000000..f470e1c01 --- /dev/null +++ b/app/core/usage/reset_credits_refresh_scheduler.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import asyncio +import contextlib +import importlib +import logging +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field +from typing import Protocol, cast + +from app.core.clients.rate_limit_reset_credits import ( + ResetCreditsResponse, + build_snapshot, + fetch_reset_credits, +) +from app.core.config.settings import get_settings +from app.core.crypto import TokenEncryptor +from app.db.models import Account, AccountStatus +from app.db.session import get_background_session +from app.modules.accounts.repository import AccountsRepository +from app.modules.rate_limit_reset_credits.store import ( + RateLimitResetCreditsStore, + get_rate_limit_reset_credits_store, +) + +logger = logging.getLogger(__name__) + +_RESET_CREDITS_SKIP_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.DEACTIVATED}) + +ResetCreditsFetchFn = Callable[..., Awaitable[ResetCreditsResponse]] + + +class _LeaderElectionLike(Protocol): + async def try_acquire(self) -> bool: ... + + +def _get_leader_election() -> _LeaderElectionLike: + module = importlib.import_module("app.core.scheduling.leader_election") + return cast(_LeaderElectionLike, module.get_leader_election()) + + +@dataclass(slots=True) +class RateLimitResetCreditsRefreshScheduler: + interval_seconds: int + enabled: bool + _task: asyncio.Task[None] | None = None + _stop: asyncio.Event = field(default_factory=asyncio.Event) + _lock: asyncio.Lock = field(default_factory=asyncio.Lock) + + async def start(self) -> None: + if not self.enabled: + return + if self._task and not self._task.done(): + return + self._stop.clear() + self._task = asyncio.create_task(self._run_loop()) + + async def stop(self) -> None: + if not self._task: + return + self._stop.set() + self._task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._task + self._task = None + + async def _run_loop(self) -> None: + while not self._stop.is_set(): + await self._refresh_once() + try: + await asyncio.wait_for(self._stop.wait(), timeout=self.interval_seconds) + except asyncio.TimeoutError: + continue + + async def _refresh_once(self) -> None: + if not await _get_leader_election().try_acquire(): + return + async with self._lock: + try: + async with get_background_session() as session: + accounts_repo = AccountsRepository(session) + accounts = await accounts_repo.list_accounts() + await refresh_reset_credits_for_accounts( + accounts=accounts, + encryptor=TokenEncryptor(), + store=get_rate_limit_reset_credits_store(), + fetch_fn=fetch_reset_credits, + ) + except Exception: + logger.exception("Reset credits refresh loop failed") + + +async def refresh_reset_credits_for_accounts( + *, + accounts: list[Account], + encryptor: TokenEncryptor, + store: RateLimitResetCreditsStore, + fetch_fn: ResetCreditsFetchFn = fetch_reset_credits, +) -> None: + """Refresh the cached reset-credits snapshot for each eligible account. + + CRITICAL invariant: this function MUST NOT mutate any account's persisted + status. On upstream error it logs and retains the prior cached snapshot + (i.e. it simply skips overwriting the cache) so account-status derivation + stays owned by usage refresh. One account failing must not abort the loop. + """ + for account in accounts: + if account.status in _RESET_CREDITS_SKIP_STATUSES: + continue + if not account.chatgpt_account_id: + continue + await _refresh_account_reset_credits(account, encryptor=encryptor, store=store, fetch_fn=fetch_fn) + + +async def _refresh_account_reset_credits( + account: Account, + *, + encryptor: TokenEncryptor, + store: RateLimitResetCreditsStore, + fetch_fn: ResetCreditsFetchFn, +) -> None: + try: + access_token = encryptor.decrypt(account.access_token_encrypted) + response = await fetch_fn(access_token, account.chatgpt_account_id) + except Exception as exc: # scheduler must never crash the loop or mutate account status + logger.warning( + "Reset credits refresh failed account_id=%s error=%s", + account.id, + exc, + ) + return + snapshot = build_snapshot(response) + await store.set(account.id, snapshot) + + +def build_rate_limit_reset_credits_scheduler() -> RateLimitResetCreditsRefreshScheduler: + settings = get_settings() + return RateLimitResetCreditsRefreshScheduler( + interval_seconds=settings.rate_limit_reset_credits_refresh_interval_seconds, + enabled=settings.rate_limit_reset_credits_refresh_enabled, + ) diff --git a/app/main.py b/app/main.py index 393110e54..4f71d98a3 100644 --- a/app/main.py +++ b/app/main.py @@ -41,6 +41,7 @@ from app.core.resilience.bulkhead import BulkheadMiddleware, get_bulkhead from app.core.resilience.memory_monitor import configure as configure_memory_monitor from app.core.usage.refresh_scheduler import build_usage_refresh_scheduler +from app.core.usage.reset_credits_refresh_scheduler import build_rate_limit_reset_credits_scheduler from app.db.session import SessionLocal, close_db, init_background_db, init_db from app.modules.accounts import api as accounts_api from app.modules.api_keys import api as api_keys_api @@ -63,6 +64,7 @@ ) from app.modules.quota_planner import api as quota_planner_api from app.modules.quota_planner.scheduler import build_quota_planner_scheduler +from app.modules.rate_limit_reset_credits import api as rate_limit_reset_credits_api from app.modules.reports import api as reports_api from app.modules.request_logs import api as request_logs_api from app.modules.runtime import api as runtime_api @@ -151,12 +153,14 @@ async def lifespan(app: FastAPI): sticky_session_cleanup_scheduler = build_sticky_session_cleanup_scheduler() quota_planner_scheduler = build_quota_planner_scheduler() auth_guardian_scheduler = build_auth_guardian_scheduler() + rate_limit_reset_credits_scheduler = build_rate_limit_reset_credits_scheduler() await usage_scheduler.start() await api_key_limit_reset_scheduler.start() await model_scheduler.start() await sticky_session_cleanup_scheduler.start() await quota_planner_scheduler.start() await auth_guardian_scheduler.start() + await rate_limit_reset_credits_scheduler.start() if settings.metrics_enabled and PROMETHEUS_AVAILABLE: import uvicorn @@ -317,6 +321,7 @@ async def _activate_bridge_membership(svc: RingMembershipService, iid: str) -> N await model_scheduler.stop() await api_key_limit_reset_scheduler.stop() await usage_scheduler.stop() + await rate_limit_reset_credits_scheduler.stop() try: await close_http_client() finally: @@ -387,6 +392,7 @@ def create_app() -> FastAPI: app.include_router(proxy_api.usage_router) app.include_router(audit_api.router) app.include_router(accounts_api.router) + app.include_router(rate_limit_reset_credits_api.router) app.include_router(dashboard_api.router) app.include_router(usage_api.router) app.include_router(request_logs_api.router) diff --git a/app/modules/accounts/mappers.py b/app/modules/accounts/mappers.py index cff607e40..ee07bf840 100644 --- a/app/modules/accounts/mappers.py +++ b/app/modules/accounts/mappers.py @@ -22,6 +22,11 @@ AccountUsageTrend, UsageTrendPoint, ) +from app.modules.rate_limit_reset_credits.store import ( + RateLimitResetCreditsSnapshot, + RateLimitResetCreditsStore, + get_rate_limit_reset_credits_store, +) from app.modules.usage.mappers import usage_history_to_window_row _ACCOUNT_ROUTING_POLICIES = frozenset({"burn_first", "normal", "preserve"}) @@ -39,7 +44,9 @@ def build_account_summaries( limit_warmups_by_account: dict[str, AccountLimitWarmup] | None = None, encryptor: TokenEncryptor, include_auth: bool = True, + reset_credits_store: RateLimitResetCreditsStore | None = None, ) -> list[AccountSummary]: + store = reset_credits_store or get_rate_limit_reset_credits_store() duplicate_keys = _duplicate_detection_keys_appearing_more_than_once(accounts) return [ _account_to_summary( @@ -53,6 +60,7 @@ def build_account_summaries( encryptor, include_auth=include_auth, is_email_duplicate=_duplicate_detection_key(account) in duplicate_keys, + reset_credits_snapshot=store.get(account.id), ) for account in accounts ] @@ -99,6 +107,7 @@ def _account_to_summary( encryptor: TokenEncryptor, include_auth: bool = True, is_email_duplicate: bool = False, + reset_credits_snapshot: RateLimitResetCreditsSnapshot | None = None, ) -> AccountSummary: plan_type = coerce_account_plan_type(account.plan_type, DEFAULT_PLAN) auth_status = _build_auth_status(account, encryptor) if include_auth else None @@ -261,6 +270,8 @@ def _account_to_summary( limit_warmup_enabled=bool(account.limit_warmup_enabled), limit_warmup=_limit_warmup_to_status(limit_warmup), is_email_duplicate=is_email_duplicate, + available_reset_credits=reset_credits_snapshot.available_count if reset_credits_snapshot else 0, + reset_credit_nearest_expires_at=(reset_credits_snapshot.nearest_expires_at if reset_credits_snapshot else None), ) diff --git a/app/modules/accounts/schemas.py b/app/modules/accounts/schemas.py index 330032dbb..af2fed7b1 100644 --- a/app/modules/accounts/schemas.py +++ b/app/modules/accounts/schemas.py @@ -115,6 +115,12 @@ class AccountSummary(DashboardModel): # surface a "delete older" action without requiring the operator to # group rows by email themselves. See codex-lb #787 (B). is_email_duplicate: bool = False + # Banked rate-limit reset credits joined from the in-memory snapshot + # (refreshed by the leader-gated reset-credits scheduler). ``0`` / ``null`` + # when no snapshot is cached yet (e.g. right after restart); the dashboard + # hides all reset affordances in that case. + available_reset_credits: int = 0 + reset_credit_nearest_expires_at: datetime | None = None class AccountsResponse(DashboardModel): diff --git a/app/modules/rate_limit_reset_credits/__init__.py b/app/modules/rate_limit_reset_credits/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/modules/rate_limit_reset_credits/api.py b/app/modules/rate_limit_reset_credits/api.py new file mode 100644 index 000000000..c96656393 --- /dev/null +++ b/app/modules/rate_limit_reset_credits/api.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Awaitable, Callable + +from fastapi import APIRouter, Depends +from pydantic import Field + +from app.core.auth.dependencies import ( + require_dashboard_write_access, + set_dashboard_error_format, + validate_dashboard_session, +) +from app.core.clients.rate_limit_reset_credits import ( + ConsumeResetCreditResponse, + RateLimitResetCreditsSnapshot, + ResetCreditItem, + consume_reset_credit, +) +from app.core.crypto import TokenEncryptor +from app.core.exceptions import DashboardConflictError, DashboardNotFoundError +from app.db.models import Account +from app.dependencies import AccountsContext, get_accounts_context +from app.modules.rate_limit_reset_credits.store import ( + RateLimitResetCreditsStore, + get_rate_limit_reset_credits_store, +) +from app.modules.shared.schemas import DashboardModel + +router = APIRouter( + prefix="/api/accounts", + tags=["dashboard"], + dependencies=[Depends(validate_dashboard_session), Depends(set_dashboard_error_format)], +) + +ConsumeFn = Callable[..., Awaitable[ConsumeResetCreditResponse]] + + +class ResetCreditItemResponse(DashboardModel): + id: str + reset_type: str | None = None + status: str | None = None + granted_at: datetime | None = None + expires_at: datetime | None = None + title: str | None = None + description: str | None = None + redeem_started_at: datetime | None = None + redeemed_at: datetime | None = None + + +class RateLimitResetCreditsSnapshotResponse(DashboardModel): + available_count: int = 0 + nearest_expires_at: datetime | None = None + credits: list[ResetCreditItemResponse] = Field(default_factory=list) + + +class ConsumeResetCreditResponseSchema(DashboardModel): + code: str | None = None + windows_reset: int | None = None + redeemed_at: datetime | None = None + + +@router.get( + "/{account_id}/rate-limit-reset-credits", + response_model=RateLimitResetCreditsSnapshotResponse | None, +) +async def get_rate_limit_reset_credits( + account_id: str, +) -> RateLimitResetCreditsSnapshotResponse | None: + snapshot = get_rate_limit_reset_credits_store().get(account_id) + return _snapshot_to_response(snapshot) + + +@router.post( + "/{account_id}/rate-limit-reset-credits/consume", + response_model=ConsumeResetCreditResponseSchema, +) +async def consume_rate_limit_reset_credit( + account_id: str, + _write_access=Depends(require_dashboard_write_access), + context: AccountsContext = Depends(get_accounts_context), +) -> ConsumeResetCreditResponseSchema: + account = await context.repository.get_by_id(account_id) + if account is None: + raise DashboardNotFoundError("Account not found", code="account_not_found") + return await _redeem_soonest_reset_credit( + account=account, + store=get_rate_limit_reset_credits_store(), + encryptor=TokenEncryptor(), + consume_fn=consume_reset_credit, + ) + + +async def _redeem_soonest_reset_credit( + *, + account: Account, + store: RateLimitResetCreditsStore, + encryptor: TokenEncryptor, + consume_fn: ConsumeFn, +) -> ConsumeResetCreditResponseSchema: + snapshot = store.get(account.id) + credit = _select_soonest_available_credit(snapshot) + if credit is None: + raise DashboardConflictError("No available reset credit", code="no_available_reset_credit") + access_token = encryptor.decrypt(account.access_token_encrypted) + result = await consume_fn(access_token, account.chatgpt_account_id, credit.id) + await store.invalidate(account.id) + redeemed_at = result.credit.redeemed_at if result.credit else None + return ConsumeResetCreditResponseSchema( + code=result.code, + windows_reset=result.windows_reset, + redeemed_at=redeemed_at, + ) + + +def _select_soonest_available_credit( + snapshot: RateLimitResetCreditsSnapshot | None, +) -> ResetCreditItem | None: + if snapshot is None: + return None + available = [credit for credit in snapshot.credits if credit.status == "available"] + if not available: + return None + far_future = datetime.max.replace(tzinfo=timezone.utc) + return min(available, key=lambda credit: credit.expires_at or far_future) + + +def _snapshot_to_response( + snapshot: RateLimitResetCreditsSnapshot | None, +) -> RateLimitResetCreditsSnapshotResponse | None: + if snapshot is None: + return None + return RateLimitResetCreditsSnapshotResponse( + available_count=snapshot.available_count, + nearest_expires_at=snapshot.nearest_expires_at, + credits=[ResetCreditItemResponse.model_validate(credit.model_dump()) for credit in snapshot.credits], + ) diff --git a/app/modules/rate_limit_reset_credits/store.py b/app/modules/rate_limit_reset_credits/store.py new file mode 100644 index 000000000..937c11974 --- /dev/null +++ b/app/modules/rate_limit_reset_credits/store.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import anyio + +from app.core.clients.rate_limit_reset_credits import RateLimitResetCreditsSnapshot + + +class RateLimitResetCreditsStore: + """In-memory cache of the most recent reset-credits snapshot per account. + + Mirrors the lock-guarded shape of :class:`RateLimitHeadersCache` / + :class:`AccountSelectionCache`. Snapshots are keyed by account id and are + repopulated by the leader-gated refresh scheduler on each tick; reads from + the dashboard (GET + the AccountSummary mapper) never hit upstream. + """ + + def __init__(self) -> None: + self._snapshots: dict[str, RateLimitResetCreditsSnapshot] = {} + self._lock = anyio.Lock() + + async def set(self, account_id: str, snapshot: RateLimitResetCreditsSnapshot) -> None: + async with self._lock: + self._snapshots[account_id] = snapshot + + def get(self, account_id: str) -> RateLimitResetCreditsSnapshot | None: + return self._snapshots.get(account_id) + + async def invalidate(self, account_id: str | None = None) -> None: + async with self._lock: + if account_id is None: + self._snapshots.clear() + return + self._snapshots.pop(account_id, None) + + +_rate_limit_reset_credits_store = RateLimitResetCreditsStore() + + +def get_rate_limit_reset_credits_store() -> RateLimitResetCreditsStore: + return _rate_limit_reset_credits_store diff --git a/frontend/src/components/confirm-dialog.tsx b/frontend/src/components/confirm-dialog.tsx index 5fbad5852..8fc767a5e 100644 --- a/frontend/src/components/confirm-dialog.tsx +++ b/frontend/src/components/confirm-dialog.tsx @@ -14,6 +14,7 @@ export type ConfirmDialogProps = { title: string; description?: string; confirmLabel?: string; + confirmDisabled?: boolean; cancelLabel?: string; onConfirm: () => void; onOpenChange: (open: boolean) => void; @@ -25,6 +26,7 @@ export function ConfirmDialog({ title, description, confirmLabel = "Confirm", + confirmDisabled = false, cancelLabel = "Cancel", onConfirm, onOpenChange, @@ -40,7 +42,15 @@ export function ConfirmDialog({ {children} {cancelLabel} - {confirmLabel} + { + event.preventDefault(); + onConfirm(); + }} + > + {confirmLabel} + diff --git a/frontend/src/features/accounts/api.ts b/frontend/src/features/accounts/api.ts index ba79bde55..9aa5d0b01 100644 --- a/frontend/src/features/accounts/api.ts +++ b/frontend/src/features/accounts/api.ts @@ -15,6 +15,7 @@ import { AccountTrendsResponseSchema, AccountProbeRequestSchema, AccountProbeResponseSchema, + ConsumeRateLimitResetCreditResponseSchema, ManualOauthCallbackRequestSchema, ManualOauthCallbackResponseSchema, OauthCompleteRequestSchema, @@ -22,6 +23,7 @@ import { OauthStartRequestSchema, OauthStartResponseSchema, OauthStatusResponseSchema, + RateLimitResetCreditsSnapshotSchema, RuntimeConnectAddressResponseSchema, } from "@/features/accounts/schemas"; import type { AccountRoutingPolicy } from "@/features/accounts/schemas"; @@ -117,6 +119,20 @@ export function exportAccountAuth(accountId: string) { ); } +export function getRateLimitResetCredits(accountId: string) { + return get( + `${ACCOUNTS_BASE_PATH}/${encodeURIComponent(accountId)}/rate-limit-reset-credits`, + RateLimitResetCreditsSnapshotSchema.nullable(), + ); +} + +export function consumeRateLimitResetCredit(accountId: string) { + return post( + `${ACCOUNTS_BASE_PATH}/${encodeURIComponent(accountId)}/rate-limit-reset-credits/consume`, + ConsumeRateLimitResetCreditResponseSchema, + ); +} + export function deleteAccount(accountId: string, deleteHistory = false) { const qs = deleteHistory ? "?delete_history=true" : ""; return del( diff --git a/frontend/src/features/accounts/components/account-actions.test.tsx b/frontend/src/features/accounts/components/account-actions.test.tsx index edc16b49d..b432b65fb 100644 --- a/frontend/src/features/accounts/components/account-actions.test.tsx +++ b/frontend/src/features/accounts/components/account-actions.test.tsx @@ -20,6 +20,7 @@ describe("AccountActions", () => { onDelete={vi.fn()} onReauth={vi.fn()} onExportAuth={vi.fn()} + onResetCredit={vi.fn()} onSecurityWorkAuthorizedChange={vi.fn()} onLimitWarmupChange={vi.fn()} onRoutingPolicyChange={onRoutingPolicyChange} @@ -46,6 +47,7 @@ describe("AccountActions", () => { onDelete={vi.fn()} onReauth={onReauth} onExportAuth={vi.fn()} + onResetCredit={vi.fn()} onSecurityWorkAuthorizedChange={vi.fn()} onLimitWarmupChange={vi.fn()} onRoutingPolicyChange={vi.fn()} @@ -78,6 +80,7 @@ describe("AccountActions", () => { onDelete={vi.fn()} onReauth={vi.fn()} onExportAuth={vi.fn()} + onResetCredit={vi.fn()} onSecurityWorkAuthorizedChange={vi.fn()} onLimitWarmupChange={vi.fn()} onRoutingPolicyChange={vi.fn()} @@ -107,6 +110,7 @@ describe("AccountActions", () => { onDelete={vi.fn()} onReauth={vi.fn()} onExportAuth={vi.fn()} + onResetCredit={vi.fn()} onSecurityWorkAuthorizedChange={vi.fn()} onLimitWarmupChange={vi.fn()} onRoutingPolicyChange={vi.fn()} @@ -138,6 +142,7 @@ describe("AccountActions", () => { onDelete={vi.fn()} onReauth={vi.fn()} onExportAuth={vi.fn()} + onResetCredit={vi.fn()} onSecurityWorkAuthorizedChange={vi.fn()} onLimitWarmupChange={vi.fn()} onRoutingPolicyChange={vi.fn()} @@ -151,4 +156,60 @@ describe("AccountActions", () => { expect(onProbe).not.toHaveBeenCalled(); }); + + it("shows reset action when reset credits are available", async () => { + const user = userEvent.setup(); + const onResetCredit = vi.fn(); + const account = createAccountSummary({ + availableResetCredits: 3, + resetCreditNearestExpiresAt: "2026-01-03T12:00:00.000Z", + }); + + render( + , + ); + + await user.click(screen.getByRole("button", { name: "Reset" })); + + expect(onResetCredit).toHaveBeenCalledWith(account.accountId); + }); + + it("hides reset action when no reset credits are available", () => { + const account = createAccountSummary({ + availableResetCredits: 0, + resetCreditNearestExpiresAt: null, + }); + + render( + , + ); + + expect(screen.queryByRole("button", { name: "Reset" })).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/features/accounts/components/account-actions.tsx b/frontend/src/features/accounts/components/account-actions.tsx index 6092b0ea8..5a6e94294 100644 --- a/frontend/src/features/accounts/components/account-actions.tsx +++ b/frontend/src/features/accounts/components/account-actions.tsx @@ -4,6 +4,7 @@ import { Pause, Play, RefreshCw, + RotateCcw, Route, ShieldCheck, Trash2, @@ -23,6 +24,7 @@ import type { AccountRoutingPolicy, AccountSummary, } from "@/features/accounts/schemas"; +import { formatSingleUnitRemaining } from "@/utils/formatters"; export type AccountActionsProps = { account: AccountSummary; @@ -34,6 +36,7 @@ export type AccountActionsProps = { onDelete: (accountId: string) => void; onReauth: () => void; onExportAuth: (accountId: string) => void; + onResetCredit: (accountId: string) => void; onSecurityWorkAuthorizedChange: (accountId: string, enabled: boolean) => void; onLimitWarmupChange: (accountId: string, enabled: boolean) => void; onRoutingPolicyChange: ( @@ -52,6 +55,7 @@ export function AccountActions({ onDelete, onReauth, onExportAuth, + onResetCredit, onSecurityWorkAuthorizedChange, onLimitWarmupChange, onRoutingPolicyChange, @@ -60,6 +64,10 @@ export function AccountActions({ account.status === "reauth_required" || account.status === "deactivated"; const probeDisabled = busy || readOnly || account.status === "paused" || showOperatorRecoveryAction; + const resetCountdown = account.resetCreditNearestExpiresAt + ? formatSingleUnitRemaining(account.resetCreditNearestExpiresAt) + : null; + const hasResetCredits = (account.availableResetCredits ?? 0) > 0; return (
@@ -191,6 +199,33 @@ export function AccountActions({ Export + {hasResetCredits ? ( + + ) : null} + + {hasResetCredits ? ( + + ) : null} {status === "paused" && ( + {hasResetCredits ? ( + + ) : null}
); } diff --git a/frontend/src/utils/formatters.test.ts b/frontend/src/utils/formatters.test.ts index 7258a84c8..1427a7f0f 100644 --- a/frontend/src/utils/formatters.test.ts +++ b/frontend/src/utils/formatters.test.ts @@ -20,6 +20,7 @@ import { formatQuotaResetMeta, formatRate, formatResetRelative, + formatSingleUnitRemaining, formatRefreshTokenLabel, formatRelative, formatTimeLong, @@ -125,6 +126,29 @@ describe("formatters", () => { expect(formatCountdown(125)).toBe("2:05"); }); + it("formats single-unit reset-credit countdowns", () => { + expect(formatSingleUnitRemaining("2026-01-08T00:00:00.000Z")).toEqual({ + label: "7d", + expiringSoon: false, + }); + expect(formatSingleUnitRemaining("2026-01-07T00:00:00.000Z")).toEqual({ + label: "6d", + expiringSoon: true, + }); + expect(formatSingleUnitRemaining("2026-01-01T01:00:00.000Z")).toEqual({ + label: "1h", + expiringSoon: true, + }); + expect(formatSingleUnitRemaining("2026-01-01T00:01:00.000Z")).toEqual({ + label: "1m", + expiringSoon: true, + }); + expect(formatSingleUnitRemaining("2025-12-31T23:59:59.000Z")).toEqual({ + label: "now", + expiringSoon: true, + }); + }); + it("formats quota reset labels", () => { const in30m = new Date(Date.now() + 30 * 60_000).toISOString(); const in4h13m = new Date(Date.now() + (4 * 60 + 13) * 60_000).toISOString(); diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts index 864d3135c..eebf38878 100644 --- a/frontend/src/utils/formatters.ts +++ b/frontend/src/utils/formatters.ts @@ -285,6 +285,35 @@ export function formatQuotaResetLabel(resetAt: string | null | undefined): strin return formatResetRelative(diffMs); } +const DAY_MS = 86_400_000; +const HOUR_MS = 3_600_000; +const MINUTE_MS = 60_000; +const EXPIRING_SOON_THRESHOLD_MS = 7 * DAY_MS; + +export type SingleUnitRemaining = { + label: string; + expiringSoon: boolean; +}; + +export function formatSingleUnitRemaining(expiresAtIso: string): SingleUnitRemaining { + const ms = new Date(expiresAtIso).getTime() - Date.now(); + if (ms <= 0) { + return { label: "now", expiringSoon: true }; + } + const days = Math.floor(ms / DAY_MS); + const hours = Math.floor(ms / HOUR_MS); + const minutes = Math.floor(ms / MINUTE_MS); + const label = + days >= 1 + ? `${days}d` + : hours >= 1 + ? `${hours}h` + : minutes >= 1 + ? `${minutes}m` + : "now"; + return { label, expiringSoon: ms < EXPIRING_SOON_THRESHOLD_MS }; +} + export function formatQuotaResetMeta( resetAtSecondary: string | null | undefined, windowMinutesSecondary: unknown, diff --git a/openspec/changes/add-rate-limit-reset-credits/.openspec.yaml b/openspec/changes/add-rate-limit-reset-credits/.openspec.yaml new file mode 100644 index 000000000..3ac681e39 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-17 diff --git a/openspec/changes/add-rate-limit-reset-credits/design.md b/openspec/changes/add-rate-limit-reset-credits/design.md new file mode 100644 index 000000000..977f07338 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/design.md @@ -0,0 +1,68 @@ +## Context + +codex-lb already polls upstream `GET /wham/usage` per account on a 60s leader-gated loop (`app/core/usage/refresh_scheduler.py`) and uses an in-memory cache pattern (`app/modules/proxy/rate_limit_cache.py`) for short-lived values. Per-account OAuth bearer tokens are stored encrypted at rest (`Account.access_token_encrypted`) and decrypted on demand via `TokenEncryptor`. Dashboard endpoints authenticate via `validate_dashboard_session` + `require_dashboard_write_access`. + +OpenAI added banked rate-limit reset credits on 2026-06-12. The upstream API surface (reverse-engineered and documented in the [`aaamosh/codex-reset`](https://github.com/aaamosh/codex-reset) reference) is: + +- `GET /wham/rate-limit-reset-credits` → `{credits: [...], available_count: N}` with `Authorization: Bearer ` + `chatgpt-account-id: ` headers +- `POST /wham/rate-limit-reset-credits/consume` with body `{credit_id, redeem_request_id}` → `{code, credit, windows_reset}` + +The reference is a single-account CLI; codex-lb needs a multi-account, dashboard-driven, in-memory-cached variant. + +## Goals / Non-Goals + +**Goals:** +- Per-account background poll of reset credits every 60s, cached in-memory keyed by account id +- Dashboard operators can see, per account: how many banked credits are available and when the soonest one expires +- Dashboard operators can redeem the soonest-expiring credit for any account from three surfaces (Accounts action bar, Dashboard table, Dashboard grid) with a confirmation dialog +- Sort the Accounts page by available reset credits +- Reuse existing token decryption, scheduler shape, in-memory cache shape, dashboard auth, confirmation dialog, and formatter conventions — no new frameworks + +**Non-Goals:** +- No DB persistence — snapshots live only in memory and are repopulated after restart +- No referral/invite logic from the reference repo +- No changes to `/wham/usage` or account status derivation (rate_limited / quota_exceeded reconciliation stays owned by usage refresh) +- No live-ticking countdown — values recompute on each 60s scheduler tick + TanStack Query refetch, matching the existing `formatQuotaResetLabel` pattern +- No new top-nav account card; the badge lives on `AccountListItem` only +- No mobile-specific behavior + +## Decisions + +### Decision: Dedicated module + scheduler, mirroring `usage_refresh` (not folding into the usage loop) +**Rationale:** Usage refresh owns account-status derivation and has a dense, scenario-heavy spec (`usage-refresh-policy`) tying it to quota reconciliation, cooldowns, and warm-up. Bolting credits onto that loop would couple two upstream calls and their failure modes, and muddy the usage-refresh contract. A dedicated `RateLimitResetCreditsRefreshScheduler` reuses the exact `UsageRefreshScheduler` shape (leader-gated, `asyncio.Lock`-guarded `_refresh_once`, `interval_seconds` + `enabled` settings) and is independently toggleable. +**Alternatives considered:** (a) Fold into `UsageRefreshScheduler._refresh_once` — rejected for the coupling above. (b) Pure passthrough via the local `wham_router` proxy — rejected because the dashboard needs the in-memory store and per-account token decryption that the proxy router does not have, and the requirement is "refresh every 60s + store in-memory." + +### Decision: Server picks the soonest-expiring credit at consume time +**Rationale:** Single source of truth. The client passes only `{account_id}` to `POST /consume`; the server reads the cached snapshot, selects the available credit with the smallest `expires_at`, generates `redeem_request_id = uuid4()`, and calls upstream. This guarantees "nearest expiry_at is selected" even if the UI is stale, and avoids a client/server clock skew race. +**Alternatives considered:** Client sends the specific `credit_id` — rejected because the cached snapshot may have changed between render and click (e.g. one expired or was redeemed elsewhere). + +### Decision: Expose `available_reset_credits` + `reset_credit_nearest_expires_at` on `AccountSummary` (no DB column) +**Rationale:** The Accounts-page and Dashboard list both consume `AccountSummary`; joining the cached snapshot at mapper time gets the data to every UI surface with one change and zero migration. Account rows that have no cache yet return `0` / `null` and the UI hides its reset affordances for them. +**Alternatives considered:** Separate `/api/accounts/{id}/rate-limit-reset-credits` GET consumed per-card — rejected because it adds N round-trips and N re-renders; the count belongs on the summary the UI already fetches. + +### Decision: Countdown is single-unit and goes red under 7 days +**Rationale:** User requirement: one unit only ("6d" / "13h" / "45m" / "now"), red when `< 7d`. A new `formatSingleUnitRemaining(expiresAtIso)` helper sits next to existing `formatQuotaResetLabel` / `formatResetRelative` in `utils/formatters.ts`; the caller colors it via `ms < 7 * DAY_MS`. We do NOT add a ticking hook (matches the existing reset-label pattern). +**Alternatives considered:** Reuse `formatResetRelative` — rejected because it returns multi-unit ("6d 13h") output. + +### Decision: Reset credit refresh never mutates account status +**Rationale:** Account status (active / rate_limited / quota_exceeded / paused / deactivated) is owned by usage refresh. Reset-credit polling failure MUST NOT deactivate or block an account — doing so would create a second status owner and contradict `usage-refresh-policy`. On upstream errors the scheduler logs, keeps the prior snapshot if any, and moves on. +**Alternatives considered:** Reuse usage-refresh cooldown/deactivation classification — rejected because it would require this scheduler to write account status, violating the single-owner invariant. + +## Risks / Trade-offs + +- **[Upstream endpoints are undocumented]** → Mitigation: client treats non-200 / non-JSON defensively, logs, keeps prior snapshot; consume-failure surfaces to UI as a toast without invalidating the cache. Document the upstream-dependence caveat in the capability `context.md`. +- **[In-memory cache lost on restart]** → Mitigation: acceptable per requirements; the next 60s tick repopulates. UI treats missing snapshot as `available_reset_credits: 0` (hidden affordances), not an error. +- **[Credit consumed even on partial reset]** (upstream behavior: "if POST returns 200, the credit is gone") → Mitigation: confirmation dialog explicitly warns that the credit is consumed regardless of whether the rate-limit window moves. On success we invalidate the cache and let the next tick reconcile. +- **[Race: credit expires between render and click]** → Mitigation: server re-selects from the freshest cached snapshot at consume time and surfaces upstream's error if the chosen credit is no longer redeemable. +- **[Many accounts = many upstream calls per tick]** → Mitigation: existing per-account usage-refresh already pays this cost on the same cadence; leader-gated so only one replica polls. Reuses the same skip rules (paused/deactivated/missing chatgpt-account-id). +- **[Guest read-only dashboard users]** → Mitigation: `POST /consume` requires `require_dashboard_write_access`; guests can see the badge/button (count is read off `AccountSummary`) but cannot redeem. + +## Migration Plan + +- No DB migration (in-memory only). No env var required (settings default to enabled + 60s). +- Deploy is a single rolling restart; the first tick after boot repopulates snapshots within 60s. +- Rollback: set `rate_limit_reset_credits_refresh_enabled=false` (or revert the deploy) — the UI hides all reset affordances as soon as the cached count is 0/missing. + +## Open Questions + +None blocking. (The two upstream endpoints' longevity is a runtime risk documented in `context.md`, not a design unknown.) diff --git a/openspec/changes/add-rate-limit-reset-credits/proposal.md b/openspec/changes/add-rate-limit-reset-credits/proposal.md new file mode 100644 index 000000000..1abdb9d0e --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/proposal.md @@ -0,0 +1,30 @@ +## Why + +OpenAI rolled out savable ("banked") rate-limit reset credits for Codex on 2026-06-12. Eligible ChatGPT plans receive credits that can be redeemed to reset rate-limit windows, but the redeem affordance only ships in the desktop app and VS Code/Cursor/Windsurf extension — not in the Codex CLI, and not in any operator surface. codex-lb operators managing many accounts have no way to see how many banked resets an account has, when they expire, or to redeem one without leaving the dashboard. We need first-class visibility and a one-click redeem action that reuses each account's existing OAuth bearer token. + +## What Changes + +- Add a per-account background poller that calls upstream `GET /wham/rate-limit-reset-credits` every 60s using each account's stored bearer token, and caches the result in-memory keyed by account id. +- Add a dashboard endpoint `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` that redeems the soonest-expiring available credit by calling upstream `POST /wham/rate-limit-reset-credits/consume` with `{credit_id, redeem_request_id}`. +- Expose `available_reset_credits` count and `reset_credit_nearest_expires_at` timestamp on the account summary payloads consumed by the Accounts page and Dashboard. +- Accounts page: add a "Reset" button to the per-account action bar (next to Export), a count badge on each `AccountListItem` (capped at "99+"), and a new "Most reset credits" option in the sort-mode dropdown. +- Dashboard Accounts section: add a "Reset" button next to Details in both the table and grid views. +- Show a single-unit countdown ("6d" / "13h" / "45m" / "now") of the nearest credit's expiry on each Reset button; render it in destructive/red when less than 7 days remain. +- Gate the Reset button and badge on `available_reset_credits > 0`; gate the redeem endpoint on dashboard write access (read-only guests cannot redeem). + +## Capabilities + +### New Capabilities +- `rate-limit-reset-credits`: Background polling, in-memory caching, and dashboard-initiated redemption of upstream Codex banked rate-limit reset credits per account. + +### Modified Capabilities +- `frontend-architecture`: New dashboard/Accounts UI elements — Reset button (Accounts tab + Dashboard), count badge on AccountListItem, expiry countdown label, and a new Accounts sort mode by available reset credits. + +## Impact + +- **Backend (new)**: `app/core/clients/rate_limit_reset_credits.py` (upstream client), `app/modules/rate_limit_reset_credits/store.py` (in-memory store + singleton), `app/core/usage/reset_credits_refresh_scheduler.py` (60s leader-gated loop), `app/modules/rate_limit_reset_credits/api.py` (dashboard GET + consume POST). +- **Backend (modified)**: `app/main.py` lifespan wiring (build/start/stop the new scheduler); `app/core/config/settings.py` (two new flags — enabled + interval seconds); account-summary mappers in `app/modules/accounts/` and the dashboard mapper to join the cached snapshot onto `AccountSummary`. +- **Frontend (new/modified)**: `features/accounts/schemas.ts` + `features/dashboard/schemas.ts` (two new fields); `features/accounts/api.ts` (consume client); `features/accounts/sorting.ts` (new sort mode); `features/accounts/components/account-actions.tsx` (Reset button); `features/accounts/components/account-list-item.tsx` (count badge); `features/dashboard/components/account-list.tsx` + `account-card.tsx` (Reset button); `utils/formatters.ts` (single-unit countdown); reuse of `components/confirm-dialog.tsx` + `hooks/use-dialog-state.ts` for the confirmation flow. +- **Upstream contract**: undocumented OpenAI endpoints under `https://chatgpt.com/backend-api/wham/rate-limit-reset-credits`; behavior is best-effort and may change upstream (see context doc). +- **In-memory only**: no DB schema migration; snapshots are lost on restart and repopulated on the next tick. +- **Tests**: pytest for client/store/scheduler/API/mapper; vitest (or equivalent) for formatter boundaries, badge cap, button visibility, confirm flow, and sort comparator. diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md new file mode 100644 index 000000000..fe1017de5 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md @@ -0,0 +1,89 @@ +## ADDED Requirements + +### Requirement: Accounts page exposes a reset-credits redeem action + +The Accounts page per-account action bar SHALL render a "Reset" button next to the existing Export button with matching button styling whenever the account reports `available_reset_credits > 0`. The button SHALL be hidden when `available_reset_credits` is `0`. Activating the button SHALL open a confirmation dialog that names the soonest-expiring credit's title and expiry, and explicitly warns that the credit is consumed regardless of whether the rate-limit window moves. Confirming SHALL submit a redeem request for that account and refresh account data on success. + +#### Scenario: Reset button mirrors Export styling and placement +- **WHEN** the Accounts page renders the per-account action bar for an account with `available_reset_credits > 0` +- **THEN** a "Reset" button appears immediately next to the Export button +- **AND** the button uses the same size, variant, and class as the Export button + +#### Scenario: Reset button hidden when no credits available +- **WHEN** an account reports `available_reset_credits: 0` +- **THEN** the per-account action bar renders no "Reset" button + +#### Scenario: Confirmation required before redeem +- **WHEN** the operator clicks the "Reset" button +- **THEN** a confirmation dialog opens describing the soonest-expiring credit and the no-refund warning +- **AND** no redeem request is sent until the operator confirms + +### Requirement: AccountListItem displays a reset-credits count badge + +The Accounts page `AccountListItem` SHALL render a count badge pinned to the right-upper radius of the item whenever the account reports `available_reset_credits > 0`. The badge SHALL display the integer count, capped visually at `"99+"` when the count exceeds 99. The badge SHALL be absent when `available_reset_credits` is `0`. + +#### Scenario: Badge shows the available count +- **WHEN** an `AccountListItem` renders for an account with `available_reset_credits: 3` +- **THEN** a count badge pinned to the item's right-upper radius displays `3` + +#### Scenario: Badge caps at 99+ +- **WHEN** an `AccountListItem` renders for an account with `available_reset_credits: 120` +- **THEN** the count badge displays `99+` + +#### Scenario: Badge absent when zero +- **WHEN** an `AccountListItem` renders for an account with `available_reset_credits: 0` +- **THEN** no count badge is rendered + +### Requirement: Accounts page can sort by available reset credits + +The Accounts page sort selector SHALL offer a "Most reset credits" option that orders accounts by `available_reset_credits` descending. Ties SHALL be broken by `reset_credit_nearest_expires_at` ascending (soonest expiring first), and accounts with no expiry SHALL sort after accounts that have one. + +#### Scenario: More available credits sorts first +- **WHEN** the operator selects "Most reset credits" +- **AND** account A has `available_reset_credits: 4` and account B has `available_reset_credits: 1` +- **THEN** account A appears before account B + +#### Scenario: Tie breaks by soonest expiry +- **WHEN** two accounts have equal `available_reset_credits` +- **AND** one account's soonest credit expires before the other's +- **THEN** the account with the earlier `reset_credit_nearest_expires_at` appears first + +### Requirement: Dashboard accounts section exposes a reset-credits redeem action + +The Dashboard Accounts section SHALL render a "Reset" button next to the existing Details action in both the table and grid views for any account with `available_reset_credits > 0`. The button SHALL be absent when `available_reset_credits` is `0`. Activating the button SHALL open the same confirmation flow as the Accounts page reset action. + +#### Scenario: Table view shows reset next to details +- **WHEN** the Dashboard Accounts section renders in table view for an account with `available_reset_credits > 0` +- **THEN** a "Reset" action appears in the same action cell as the Details action + +#### Scenario: Grid view shows reset next to details +- **WHEN** the Dashboard Accounts section renders in grid view for an account with `available_reset_credits > 0` +- **THEN** a "Reset" button appears next to the Details button on the account card + +#### Scenario: Reset action absent when no credits +- **WHEN** an account reports `available_reset_credits: 0` +- **THEN** the Dashboard Accounts section renders no "Reset" action for that account in either view + +### Requirement: Reset actions display a single-unit expiry countdown + +Every "Reset" button SHALL display a small countdown label of the soonest-expiring credit's expiry, formatted as a single time unit: `"${d}d"` for any remaining duration of one day or more, `"${h}h"` for durations under one day but at least one hour, `"${m}m"` for durations under one hour but at least one minute, and `"now"` for durations under one minute. The label SHALL render in the destructive/red color when the remaining duration is strictly less than 7 days, and in the default muted color otherwise. + +#### Scenario: Days format for duration at or above one day +- **WHEN** a Reset button renders for a credit whose `expires_at` is 12 days away +- **THEN** the countdown label reads `12d` +- **AND** the label uses the default muted color + +#### Scenario: Red color under seven days +- **WHEN** a Reset button renders for a credit whose `expires_at` is 6 days away +- **THEN** the countdown label reads `6d` +- **AND** the label uses the destructive/red color + +#### Scenario: Hours and minutes use the smaller unit +- **WHEN** a Reset button renders for a credit whose `expires_at` is 13 hours away +- **THEN** the countdown label reads `13h` +- **AND** the label uses the destructive/red color + +#### Scenario: Sub-minute duration shows now +- **WHEN** a Reset button renders for a credit whose `expires_at` is 30 seconds away +- **THEN** the countdown label reads `now` +- **AND** the label uses the destructive/red color diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md new file mode 100644 index 000000000..729116221 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md @@ -0,0 +1,110 @@ +# Rate-Limit Reset Credits Context + +## Purpose + +codex-lb polls OpenAI's banked ("savable") rate-limit reset credits per account, caches them +in memory, and lets dashboard operators redeem the soonest-expiring credit for any account +without leaving the dashboard. The credit is a ChatGPT-subscription entitlement granted by +OpenAI; codex-lb is spending a credit OpenAI already gave the account — it does not bypass +any rate limit. + +## Upstream Source + +The credits endpoints live under `https://chatgpt.com/backend-api/wham`: + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/wham/rate-limit-reset-credits` | GET | List banked credits + `available_count` | +| `/wham/rate-limit-reset-credits/consume` | POST | Redeem one credit (body: `credit_id`, `redeem_request_id`) | + +Both require `Authorization: Bearer ` and `chatgpt-account-id: ` +headers. The consume body returns `{code, credit: {id, status, redeemed_at, ...}, windows_reset}`. + +These endpoints are undocumented and were reverse-engineered from the official +`openai.chatgpt` VS Code extension's webview bundle. The canonical external reference is +[`aaamosh/codex-reset`](https://github.com/aaamosh/codex-reset) — a single-account CLI +implementation that codex-lb's multi-account, dashboard-driven, in-memory-cached variant +is based on. OpenAI may rename, gate, or remove these endpoints at any time; the codex-lb +client treats non-200/non-JSON responses defensively. + +## Decisions + +- **In-memory only.** No DB column, no migration. Snapshots repopulate within one tick of + startup. Restart cost: up to 60s of `available_reset_credits: 0` everywhere. +- **Server picks the credit, not the client.** `POST /consume` takes only the account id; + the server selects the soonest-expiring available credit from the freshest snapshot and + generates the `redeem_request_id`. Avoids stale-UI and clock-skew races. +- **Never mutates account status.** Account status is owned by usage refresh + (see `usage-refresh-policy`). Reset-credit polling failure logs and retains the prior + snapshot; it does not deactivate, rate-limit, or quota-block any account. +- **Dedicated scheduler, not folded into usage refresh.** Reuses the exact + `UsageRefreshScheduler` shape (leader-gated, `asyncio.Lock`-guarded, configurable + cadence) but keeps the two upstream calls decoupled. See `design.md` for the rationale. + +## Failure Modes + +- **Upstream returns 200 but the rate-limit window doesn't move.** Per upstream behavior + the credit is still consumed. The confirmation dialog warns the operator; on success we + invalidate the cache and let the next tick reconcile `available_count`. +- **Snapshot is empty/stale.** UI hides all reset affordances for that account + (`available_reset_credits: 0`). Not an error — wait one tick. +- **Upstream 401/403/auth-expired.** Logged; prior snapshot retained. Does NOT deactivate + the account. If the token is genuinely expired, usage refresh / OAuth refresh owns the + deactivation path. +- **Concurrent consume clicks.** Server re-selects from the snapshot each call; the second + click either consumes a different credit (if multiple were available) or surfaces + upstream's "no available credit" error. + +## Example: list response + +```json +{ + "credits": [ + { + "id": "RateLimitResetCredit_test", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-12T01:29:41.346025Z", + "expires_at": "2026-07-12T01:29:41.346025Z", + "redeem_started_at": null, + "redeemed_at": null, + "profile_image_url": "https://openaiassets.blob.core.windows.net/$web/codex/codex-icon-200.png", + "profile_user_id": "Codex Team", + "title": "One free rate limit reset", + "description": "Thanks for using Codex! You've been granted one free rate limit reset." + } + ], + "available_count": 1 +} +``` + +## Example: consume response + +```json +{ + "code": "reset", + "credit": { + "id": "RateLimitResetCredit_...", + "reset_type": "codex_rate_limits", + "status": "redeemed", + "redeemed_at": "2026-06-13T13:12:31Z" + }, + "windows_reset": 1 +} +``` + +## Operational Notes + +- Toggle polling without a deploy by setting `rate_limit_reset_credits_refresh_enabled=false` + and restarting. The store empties and all UI reset affordances disappear. +- The 60s cadence matches usage refresh; both are leader-gated, so adding accounts scales + upstream load by the same factor usage refresh already does. +- A credit is consumed as soon as upstream returns 200 — treat the confirmation dialog as + the point of no return. + +## Related Work + +- Reference CLI: [`aaamosh/codex-reset`](https://github.com/aaamosh/codex-reset) +- Sibling capability: [`usage-refresh-policy`](../../specs/usage-refresh-policy/) — owns + account-status derivation and the `/wham/usage` 60s polling pattern this mirrors +- OpenAI announcement: [Flexible rate-limit resets for Codex](https://community.openai.com/t/flexible-rate-limit-resets-for-codex/1383470) diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md new file mode 100644 index 000000000..3a6b1dee5 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md @@ -0,0 +1,87 @@ +## ADDED Requirements + +### Requirement: Reset credits are polled per account on a fixed cadence + +The system SHALL poll upstream `GET /wham/rate-limit-reset-credits` for each eligible account on a configurable cadence that defaults to 60 seconds, using that account's stored OAuth bearer token and `chatgpt-account-id`. The poll SHALL be leader-gated so that only one replica performs the polling in a multi-replica deployment. The poll SHALL skip any account that is paused, deactivated, or lacks a usable `chatgpt-account-id`. + +#### Scenario: Default cadence polls every 60 seconds +- **WHEN** the reset-credits refresh scheduler is enabled with default settings +- **THEN** each eligible account's credits are fetched from upstream at most once per 60 seconds + +#### Scenario: Non-leader replica does not poll +- **WHEN** leader election reports the current replica is not the leader +- **THEN** the scheduler performs no upstream reset-credits fetches in that cycle + +#### Scenario: Paused and deactivated accounts are skipped +- **WHEN** an account is persisted as `paused` or `deactivated` +- **THEN** the scheduler performs no upstream reset-credits fetch for that account +- **AND** the cached snapshot for that account (if any) is left untouched by the skip + +### Requirement: Reset credit snapshots are cached in memory keyed by account + +The system SHALL store the most recent successful reset-credits response per account in an in-memory store keyed by account id. The store SHALL be concurrency-safe and SHALL provide an `invalidate(account_id)` operation. Account-summary mappers SHALL join the cached snapshot onto each account summary, exposing `available_reset_credits` (integer) and `reset_credit_nearest_expires_at` (ISO timestamp or null). Accounts with no cached snapshot SHALL expose `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null`. + +#### Scenario: Account summary reflects cached credits +- **GIVEN** an account has a cached reset-credits snapshot with `available_count: 2` and a soonest expiry of `2026-07-10T00:00:00Z` +- **WHEN** the account-summary mapper builds the summary for that account +- **THEN** the summary exposes `available_reset_credits: 2` and `reset_credit_nearest_expires_at: "2026-07-10T00:00:00Z"` + +#### Scenario: Missing cache presents as zero credits +- **GIVEN** an account has no cached reset-credits snapshot (e.g. immediately after restart) +- **WHEN** the account-summary mapper builds the summary for that account +- **THEN** the summary exposes `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null` + +#### Scenario: Invalidate forces re-fetch on next tick +- **WHEN** a caller invokes `invalidate(account_id)` for an account +- **THEN** subsequent reads for that account return no cached snapshot +- **AND** the next scheduler tick fetches a fresh snapshot from upstream + +### Requirement: Operators can redeem the soonest-expiring available credit + +The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` that redeems exactly one credit for the named account. The endpoint SHALL select, from the freshest cached snapshot, the credit whose `status` is `available` with the smallest `expires_at`, generate a `redeem_request_id` (UUID v4), and forward `{credit_id, redeem_request_id}` to upstream `POST /wham/rate-limit-reset-credits/consume` using the account's bearer token and `chatgpt-account-id`. On a 200 response the endpoint SHALL invalidate the cached snapshot for that account and return `{code, windows_reset, redeemed_at}`. The endpoint SHALL require dashboard write access; read-only guests MUST be refused. + +#### Scenario: Consume selects the soonest-expiring credit +- **GIVEN** an account has cached credits with expiries `2026-07-10Z` and `2026-06-20Z`, both `status: available` +- **WHEN** the operator invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` +- **THEN** the request forwarded to upstream carries the `credit_id` whose `expires_at` is `2026-06-20Z` + +#### Scenario: Successful consume invalidates the cache +- **GIVEN** the operator invokes consume for an account with at least one available credit +- **WHEN** upstream returns `200` with `{code: "reset", windows_reset: 1, credit: {...}}` +- **THEN** the cached snapshot for that account is invalidated +- **AND** the response returned to the dashboard is `{code, windows_reset, redeemed_at}` derived from the upstream response + +#### Scenario: Read-only guests cannot redeem +- **GIVEN** a dashboard session authenticated as a read-only guest +- **WHEN** the guest invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` +- **THEN** the request is refused before any upstream call is made + +#### Scenario: Consume with no available credit returns a client error +- **GIVEN** an account whose cached snapshot reports `available_count: 0` (or has no snapshot) +- **WHEN** the operator invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` +- **THEN** the endpoint returns a `409` (or equivalent client-error) without calling upstream + +### Requirement: Reset credit polling failure does not mutate account status + +The reset-credits refresh scheduler SHALL NOT transition any account's persisted status (`active`, `rate_limited`, `quota_exceeded`, `paused`, `deactivated`) in response to upstream reset-credits responses. On upstream error (non-200, non-JSON, network, or auth-like failure) the scheduler SHALL log the failure and either keep the prior cached snapshot or leave the cache unset; it SHALL NOT propagate the failure to account-status derivation. + +#### Scenario: Upstream 401 on reset-credits does not deactivate the account +- **WHEN** the scheduler receives an HTTP `401` from `GET /wham/rate-limit-reset-credits` for an account +- **THEN** the account's persisted status is unchanged +- **AND** any prior cached snapshot for that account is retained + +#### Scenario: Upstream 5xx retains the prior snapshot +- **GIVEN** an account has a cached snapshot from a prior successful tick +- **WHEN** the scheduler receives an HTTP `503` on the next reset-credits tick +- **THEN** the cached snapshot is retained +- **AND** the failure is logged + +### Requirement: Reset credit polling is independently toggleable + +The system SHALL expose settings `rate_limit_reset_credits_refresh_enabled` (default `true`) and `rate_limit_reset_credits_refresh_interval_seconds` (default `60`). When disabled, the scheduler SHALL perform no upstream reset-credits fetches and the in-memory store SHALL remain empty; the dashboard SHALL render zero reset affordances for every account. + +#### Scenario: Disabled scheduler produces empty store +- **GIVEN** `rate_limit_reset_credits_refresh_enabled` is `false` +- **WHEN** the application starts and runs +- **THEN** no upstream reset-credits fetches are performed +- **AND** every account summary exposes `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null` diff --git a/openspec/changes/add-rate-limit-reset-credits/tasks.md b/openspec/changes/add-rate-limit-reset-credits/tasks.md new file mode 100644 index 000000000..8b2202f6f --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/tasks.md @@ -0,0 +1,50 @@ +## 1. Backend foundation (settings, upstream client, in-memory store) + +- [x] 1.1 Add settings `rate_limit_reset_credits_refresh_enabled` (default `true`) and `rate_limit_reset_credits_refresh_interval_seconds` (default `60`) to `app/core/config/settings.py`, mirroring the usage-refresh flags +- [x] 1.2 Create `app/core/clients/rate_limit_reset_credits.py` mirroring `app/core/clients/usage.py`: `fetch_reset_credits(access_token, account_id, *, base_url, timeout)` → GET `/wham/rate-limit-reset-credits`, and `consume_reset_credit(access_token, account_id, credit_id, *, base_url, timeout)` → POST `/wham/rate-limit-reset-credits/consume` with body `{credit_id, redeem_request_id: uuid4()}`. Reuse the same header-construction rules (skip `chatgpt-account-id` for `email_`/`local_` prefixes) and base-url normalization +- [x] 1.3 Define pydantic models for the upstream payloads: `ResetCreditItem` (id, reset_type, status, granted_at, expires_at, title, description, redeem_started_at, redeemed_at), `ResetCreditsResponse` (credits: list, available_count: int), `ConsumeResetCreditResponse` (code, credit, windows_reset) +- [x] 1.4 Create `app/modules/rate_limit_reset_credits/store.py` mirroring `app/modules/proxy/rate_limit_cache.py`: `RateLimitResetCreditsStore` with `anyio.Lock`-guarded `set(account_id, snapshot)`, `get(account_id) -> Snapshot | None`, `invalidate(account_id=None)`. Snapshot exposes `available_count`, `nearest_expires_at`, and the items list. Expose a module-level singleton + `get_rate_limit_reset_credits_store()` accessor + +## 2. Backend scheduler, API, mapper, lifespan wiring + +- [x] 2.1 Create `app/core/usage/reset_credits_refresh_scheduler.py` mirroring `app/core/usage/refresh_scheduler.py`: `RateLimitResetCreditsRefreshScheduler` dataclass with leader-gated, `asyncio.Lock`-guarded `_refresh_once` that lists accounts, skips paused/deactivated/missing-`chatgpt-account-id`, decrypts `access_token_encrypted`, calls `fetch_reset_credits`, and stores the snapshot. On upstream error: log + retain prior snapshot; do NOT mutate account status. Add `build_rate_limit_reset_credits_scheduler()` factory +- [x] 2.2 Wire the new scheduler into `app/main.py` lifespan alongside `usage_scheduler`: build (~line 148), start (~154), stop (~314) +- [x] 2.3 Create `app/modules/rate_limit_reset_credits/api.py` with `GET /api/accounts/{account_id}/rate-limit-reset-credits` (returns cached snapshot or `null`) and `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` (selects soonest-`expires_at` available credit from the freshest snapshot, generates `redeem_request_id`, calls upstream, invalidates the cached snapshot, returns `{code, windows_reset, redeemed_at}`). Use `validate_dashboard_session` for GET and `require_dashboard_write_access` for POST. Return `409` when no credit is available. Register the router in `app/main.py` +- [x] 2.4 Extend the AccountSummary mapper(s) in `app/modules/accounts/` and the dashboard mapper to join the cached snapshot onto each returned account: add `available_reset_credits: int` (0 when no snapshot) and `reset_credit_nearest_expires_at: datetime | None` (null when no snapshot) +- [x] 2.5 Update the backend pydantic response schemas (`AccountSummary` / equivalent) to declare the two new fields + +## 3. Frontend schemas, API client, formatter + +- [x] 3.1 Add `availableResetCredits: number` and `resetCreditNearestExpiresAt: string | null` to `AccountSummary` in both `frontend/src/features/accounts/schemas.ts` and `frontend/src/features/dashboard/schemas.ts` +- [x] 3.2 Add `consumeRateLimitResetCredit(accountId): Promise<{ code: string; windowsReset: number; redeemedAt: string }>` to `frontend/src/features/accounts/api.ts` posting to `/api/accounts/{id}/rate-limit-reset-credits/consume`. On success, invalidate the `['accounts']` and `['dashboard']` TanStack Query keys +- [x] 3.3 Add `formatSingleUnitRemaining(expiresAtIso: string): { label: string; expiringSoon: boolean }` to `frontend/src/utils/formatters.ts`: `"${d}d"` for ≥1 day, `"${h}h"` for ≥1 hour, `"${m}m"` for ≥1 minute, `"now"` otherwise; `expiringSoon = ms < 7 * 86_400_000`. Sit it next to the existing `formatResetRelative`/`formatQuotaResetLabel` helpers + +## 4. Frontend UI components + +- [x] 4.1 Add the count badge to `frontend/src/features/accounts/components/account-list-item.tsx`: an absolutely-positioned circle on the right-upper radius showing the integer count or `"99+"` when `> 99`. Render only when `availableResetCredits > 0` +- [x] 4.2 Add the "Reset" button to `frontend/src/features/accounts/components/account-actions.tsx` immediately after the Export button, matching its `size="sm" variant="outline" className="h-8 gap-1.5 text-xs"` style, with a `RotateCcw` icon, a single-unit countdown label (using 3.3) placed at the button's right-upper radius, and destructive/red label color when `expiringSoon`. Render only when `availableResetCredits > 0`. Wire `onClick` to open the confirmation dialog +- [x] 4.3 Add a "Reset" action to `frontend/src/features/dashboard/components/account-list.tsx` (table view) inside the existing Details action cell, matching the `h-7 w-7` icon-button style with the countdown as a `title` tooltip. Render only when `availableResetCredits > 0` +- [x] 4.4 Add a "Reset" button to `frontend/src/features/dashboard/components/account-card.tsx` (grid view) next to the Details button, matching the `h-7 gap-1.5` text style with the single-unit countdown label. Render only when `availableResetCredits > 0` +- [x] 4.5 Implement the confirmation dialog (reuse `frontend/src/components/confirm-dialog.tsx` + `frontend/src/hooks/use-dialog-state.ts`, same shape as the delete-account dialog): body shows the soonest credit's title and `expires_at`, plus the "credit is consumed even if the window doesn't move" warning. On confirm → call `consumeRateLimitResetCredit(accountId)` → success/failure toast → query invalidation +- [x] 4.6 Add a "Most reset credits" option to the Accounts page sort selector in `frontend/src/features/accounts/sorting.ts`: comparator orders by `availableResetCredits` desc, tiebreak by `resetCreditNearestExpiresAt` asc (soonest first), accounts with null expiry last. Add the localized dropdown label + +## 5. Tests + +- [x] 5.1 Backend — `app/core/clients/rate_limit_reset_credits.py`: header construction (account-id skip rule), base-url normalization, consume body shape, JSON parse on 200, error handling on non-200/non-JSON +- [x] 5.2 Backend — `app/modules/rate_limit_reset_credits/store.py`: `set`/`get`/`invalidate` (single + all), concurrency under `anyio.Lock`, missing-account returns `None` +- [x] 5.3 Backend — `reset_credits_refresh_scheduler.py`: leader-gate skip, paused/deactivated account skip, one-account failure doesn't break the loop, upstream error retains prior snapshot, account status is never mutated +- [x] 5.4 Backend — `rate_limit_reset_credits/api.py`: GET returns cached snapshot / `null` on miss; POST selects soonest expiry, calls upstream with fresh `redeem_request_id`, invalidates cache, returns `{code, windows_reset, redeemed_at}`; write-access gating refuses guests; `409` when no available credit +- [x] 5.5 Backend — AccountSummary mapper: exposes the two new fields from a cached snapshot, returns `0`/`null` when no snapshot, does not crash when store is empty +- [x] 5.6 Frontend — `formatSingleUnitRemaining`: boundaries at 7d (color flip), 1d, 1h, 1m, and `now`; sub-minute and past timestamps both yield `"now"` +- [x] 5.7 Frontend — `AccountListItem` badge: renders count, `"99+"` at 100+, absent at 0 +- [x] 5.8 Frontend — Reset button visibility: rendered when `availableResetCredits > 0`, absent at 0, in all three surfaces (account-actions, dashboard table, dashboard grid) +- [x] 5.9 Frontend — confirm dialog → consume: confirmation calls `consumeRateLimitResetCredit`, success path invalidates queries, failure path surfaces a toast and does not invalidate +- [x] 5.10 Frontend — "Most reset credits" sort: comparator orders by count desc with soonest-expiry tiebreak, null-expiry accounts last + +## 6. Validation and OpenSpec hygiene + +- [x] 6.1 Run `openspec validate add-rate-limit-reset-credits --strict` and resolve any findings +- [x] 6.2 Run `openspec validate --specs --strict` to confirm no main-spec drift +- [ ] 6.3 Run backend checks: `uv run ruff check && uv run ruff format --check && uv run pytest` (or the repo's documented equivalent) +- [x] 6.4 Run frontend checks: `pnpm -C frontend lint && pnpm -C frontend typecheck && pnpm -C frontend test` (or the repo's documented equivalent) +- [ ] 6.5 Manually verify the three Reset button placements, the badge cap behavior, the countdown color flip at 7d, the confirm flow, and the new sort option against the spec scenarios diff --git a/tests/conftest.py b/tests/conftest.py index bb7720f8d..df14c22c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ ) os.environ["CODEX_LB_UPSTREAM_BASE_URL"] = "https://example.invalid/backend-api" os.environ["CODEX_LB_USAGE_REFRESH_ENABLED"] = "false" +os.environ["CODEX_LB_RATE_LIMIT_RESET_CREDITS_REFRESH_ENABLED"] = "false" os.environ["CODEX_LB_MODEL_REGISTRY_ENABLED"] = "false" os.environ["CODEX_LB_STICKY_SESSION_CLEANUP_ENABLED"] = "false" os.environ["CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_ENABLED"] = "false" diff --git a/tests/unit/test_rate_limit_reset_credits_api.py b/tests/unit/test_rate_limit_reset_credits_api.py new file mode 100644 index 000000000..1d27809df --- /dev/null +++ b/tests/unit/test_rate_limit_reset_credits_api.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +from datetime import datetime +from types import SimpleNamespace +from typing import Any, cast + +import pytest + +from app.core.auth.dependencies import require_dashboard_write_access +from app.core.clients.rate_limit_reset_credits import ( + ConsumeResetCreditResponse, + RateLimitResetCreditsSnapshot, + ResetCreditItem, +) +from app.core.crypto import TokenEncryptor +from app.core.exceptions import DashboardConflictError, DashboardNotFoundError, DashboardPermissionError +from app.db.models import Account, AccountStatus +from app.modules.rate_limit_reset_credits import api as reset_credits_api +from app.modules.rate_limit_reset_credits.api import ( + ConsumeResetCreditResponseSchema, + _redeem_soonest_reset_credit, + _select_soonest_available_credit, + consume_rate_limit_reset_credit, + get_rate_limit_reset_credits, +) +from app.modules.rate_limit_reset_credits.store import RateLimitResetCreditsStore + +pytestmark = pytest.mark.unit + + +class StubEncryptor(TokenEncryptor): + def __init__(self) -> None: + # Skip key-file I/O; tests only exercise decrypt(). + pass + + def decrypt(self, encrypted: bytes) -> str: + return "decrypted-access-token" + + +def _account(account_id: str = "acc_1") -> Account: + return Account( + id=account_id, + chatgpt_account_id="workspace-1", + email=f"{account_id}@example.com", + plan_type="plus", + access_token_encrypted=b"encrypted", + refresh_token_encrypted=b"refresh", + id_token_encrypted=b"id", + last_refresh=datetime(2025, 1, 1), + status=AccountStatus.ACTIVE, + ) + + +def _credit( + credit_id: str, + *, + status: str = "available", + expires_at: str | None = "2026-07-12T00:00:00Z", +) -> ResetCreditItem: + return ResetCreditItem.model_validate({"id": credit_id, "status": status, "expires_at": expires_at}) + + +def _snapshot(credits: list[ResetCreditItem], available_count: int | None = None) -> RateLimitResetCreditsSnapshot: + expiries = [ + credit.expires_at for credit in credits if credit.status == "available" and credit.expires_at is not None + ] + return RateLimitResetCreditsSnapshot( + available_count=available_count if available_count is not None else len(credits), + nearest_expires_at=min(expiries) if expiries else None, + credits=credits, + ) + + +# --- GET endpoint --- + + +@pytest.mark.asyncio +async def test_get_returns_null_when_no_snapshot_cached(monkeypatch: pytest.MonkeyPatch) -> None: + store = RateLimitResetCreditsStore() + # Point the module-level singleton accessor at an empty store for isolation. + monkeypatch.setattr(reset_credits_api, "get_rate_limit_reset_credits_store", lambda: store) + response = await get_rate_limit_reset_credits("acc_missing") + assert response is None + + +@pytest.mark.asyncio +async def test_get_returns_cached_snapshot_shape(monkeypatch: pytest.MonkeyPatch) -> None: + store = RateLimitResetCreditsStore() + await store.set( + "acc_1", + _snapshot([_credit("c1"), _credit("c2", expires_at="2026-06-20T00:00:00Z")], available_count=2), + ) + monkeypatch.setattr(reset_credits_api, "get_rate_limit_reset_credits_store", lambda: store) + response = await get_rate_limit_reset_credits("acc_1") + + assert response is not None + assert response.available_count == 2 + assert response.nearest_expires_at is not None + assert {credit.id for credit in response.credits} == {"c1", "c2"} + + +# --- soonest-available selection helper --- + + +def test_select_soonest_available_credit_picks_smallest_expires_at() -> None: + snapshot = _snapshot( + [ + _credit("late", expires_at="2026-07-10T00:00:00Z"), + _credit("soon", expires_at="2026-06-20T00:00:00Z"), + _credit("used", status="redeemed", expires_at="2026-06-01T00:00:00Z"), + ] + ) + + selected = _select_soonest_available_credit(snapshot) + + assert selected is not None + assert selected.id == "soon" + + +def test_select_soonest_available_credit_returns_none_when_no_snapshot() -> None: + assert _select_soonest_available_credit(None) is None + + +def test_select_soonest_available_credit_returns_none_when_none_available() -> None: + snapshot = _snapshot([_credit("c1", status="redeemed")]) + assert _select_soonest_available_credit(snapshot) is None + + +# --- POST consume: helper covers selection, uuid body, invalidation, shape --- + + +@pytest.mark.asyncio +async def test_redeem_returns_409_when_no_available_credit() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_1", _snapshot([_credit("c1", status="redeemed")])) + + with pytest.raises(DashboardConflictError) as excinfo: + await _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=_raise_not_called, # type: ignore[arg-type] + ) + assert excinfo.value.code == "no_available_reset_credit" + + +@pytest.mark.asyncio +async def test_redeem_returns_409_when_snapshot_missing() -> None: + store = RateLimitResetCreditsStore() + with pytest.raises(DashboardConflictError): + await _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=_raise_not_called, # type: ignore[arg-type] + ) + + +@pytest.mark.asyncio +async def test_redeem_selects_soonest_calls_upstream_and_invalidates_cache() -> None: + store = RateLimitResetCreditsStore() + await store.set( + "acc_1", + _snapshot( + [ + _credit("late", expires_at="2026-07-10T00:00:00Z"), + _credit("soon", expires_at="2026-06-20T00:00:00Z"), + ] + ), + ) + + captured: dict[str, Any] = {} + + async def consume_fn(access_token: str, account_id: str | None, credit_id: str) -> ConsumeResetCreditResponse: + captured.update({"access_token": access_token, "account_id": account_id, "credit_id": credit_id}) + return ConsumeResetCreditResponse.model_validate( + { + "code": "reset", + "credit": {"id": credit_id, "status": "redeemed", "redeemed_at": "2026-06-13T13:12:31Z"}, + "windows_reset": 1, + } + ) + + result = await _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=consume_fn, + ) + + # The soonest-expiring credit id was forwarded with the decrypted token + workspace id. + assert captured == { + "access_token": "decrypted-access-token", + "account_id": "workspace-1", + "credit_id": "soon", + } + # Cache was invalidated for the account after success. + assert store.get("acc_1") is None + # Response shape matches the documented {code, windows_reset, redeemed_at}. + assert isinstance(result, ConsumeResetCreditResponseSchema) + assert result.code == "reset" + assert result.windows_reset == 1 + assert result.redeemed_at is not None + assert result.redeemed_at.year == 2026 + + +# --- POST consume: handler-level 404 when account missing --- + + +@pytest.mark.asyncio +async def test_consume_handler_returns_404_when_account_missing() -> None: + class _Repo: + async def get_by_id(self, account_id: str) -> Account | None: + return None + + fake_context = SimpleNamespace(repository=_Repo()) + + with pytest.raises(DashboardNotFoundError): + await consume_rate_limit_reset_credit( + account_id="missing", + _write_access=None, + context=cast(Any, fake_context), + ) + + +# --- POST consume: write-access gating refuses guests (full ASGI path) --- + + +@pytest.mark.asyncio +async def test_consume_refuses_read_only_guest(app_instance, async_client) -> None: # type: ignore[no-untyped-def] + async def _guest_refused(_request: Any = None) -> None: + raise DashboardPermissionError( + "Read-only dashboard access cannot modify dashboard state", + code="read_only_access", + ) + + app_instance.dependency_overrides[require_dashboard_write_access] = _guest_refused + try: + response = await async_client.post("/api/accounts/acc_guest/rate-limit-reset-credits/consume") + finally: + app_instance.dependency_overrides.pop(require_dashboard_write_access, None) + + assert response.status_code == 403 + + +async def _raise_not_called(*args: Any, **kwargs: Any) -> Any: + raise AssertionError("consume_fn must not be called when no credit is available") diff --git a/tests/unit/test_rate_limit_reset_credits_client.py b/tests/unit/test_rate_limit_reset_credits_client.py new file mode 100644 index 000000000..1c6e4211d --- /dev/null +++ b/tests/unit/test_rate_limit_reset_credits_client.py @@ -0,0 +1,361 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, cast +from uuid import UUID + +import pytest + +from app.core.clients.headers import build_chatgpt_auth_headers +from app.core.clients.rate_limit_reset_credits import ( + ConsumeResetCreditError, + ConsumeResetCreditResponse, + ResetCreditFetchError, + ResetCreditsResponse, + build_snapshot, + consume_reset_credit, + fetch_reset_credits, +) +from app.core.clients.usage import _usage_headers + +pytestmark = pytest.mark.unit + + +class StubResponse: + def __init__(self, status: int, payload: dict | None, text: str) -> None: + self.status = status + self._payload = payload + self._text = text + + async def json(self, content_type: str | None = None) -> dict: + if self._payload is None: + raise ValueError("no json") + return self._payload + + async def text(self) -> str: + return self._text + + +@dataclass +class ClientState: + calls: int = 0 + method: str | None = None + url: str | None = None + headers: dict[str, str] | None = None + json_body: dict[str, Any] | None = None + + +class StubRequestContext: + def __init__( + self, + responses: list[StubResponse], + state: ClientState, + method: str, + url: str, + headers: dict[str, str], + json_body: dict[str, Any] | None, + retry_options: object | None, + ) -> None: + self._responses = responses + self._state = state + self._method = method + self._url = url + self._headers = headers + self._json_body = json_body + self._retry_options = retry_options + + async def __aenter__(self) -> StubResponse: + attempts = getattr(self._retry_options, "attempts", 1) + statuses = set(getattr(self._retry_options, "statuses", set())) + response: StubResponse | None = None + for attempt in range(attempts): + index = min(self._state.calls, len(self._responses) - 1) + response = self._responses[index] + self._state.calls += 1 + self._state.method = self._method + self._state.url = self._url + self._state.headers = dict(self._headers) + self._state.json_body = dict(self._json_body) if self._json_body else None + if response.status in statuses and attempt < attempts - 1: + continue + return response + if response is None: + response = StubResponse(500, None, "no response") + return response + + async def __aexit__(self, exc_type, exc, tb) -> bool: + return False + + +class StubRetryClient: + def __init__(self, responses: list[StubResponse], state: ClientState) -> None: + self._responses = responses + self._state = state + + def request( + self, + method: str, + url: str, + headers: dict[str, str] | None = None, + json: dict[str, Any] | None = None, + timeout: object | None = None, + retry_options: object | None = None, + ) -> StubRequestContext: + return StubRequestContext( + self._responses, + self._state, + method, + url, + headers or {}, + json, + retry_options, + ) + + +def _list_payload() -> dict: + return { + "credits": [ + { + "id": "RateLimitResetCredit_test", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-12T01:29:41.346025Z", + "expires_at": "2026-07-12T01:29:41.346025Z", + "redeem_started_at": None, + "redeemed_at": None, + "title": "One free rate limit reset", + "description": "Thanks for using Codex!", + } + ], + "available_count": 1, + } + + +def test_usage_headers_delegate_to_shared_helper() -> None: + """The usage client and the reset-credits client share one header builder.""" + assert _usage_headers("tok", "acc_workspace") == build_chatgpt_auth_headers("tok", "acc_workspace") + + +@pytest.mark.asyncio +async def test_fetch_reset_credits_sends_bearer_and_account_id_headers() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(200, _list_payload(), "")], state) + + data = await fetch_reset_credits( + "access-token", + "acc_workspace", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert isinstance(data, ResetCreditsResponse) + assert data.available_count == 1 + assert data.credits[0].id == "RateLimitResetCredit_test" + assert state.method == "GET" + assert state.url == "http://upstream.test/backend-api/wham/rate-limit-reset-credits" + assert state.headers is not None + assert state.headers["Authorization"] == "Bearer access-token" + assert state.headers["chatgpt-account-id"] == "acc_workspace" + + +@pytest.mark.asyncio +async def test_fetch_reset_credits_skips_account_id_header_for_email_and_local_prefixes() -> None: + for account_id in ("email_user@example.com", "local_abcd"): + state = ClientState() + client = StubRetryClient([StubResponse(200, _list_payload(), "")], state) + await fetch_reset_credits( + "access-token", + account_id, + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + assert state.headers is not None + assert "chatgpt-account-id" not in state.headers, account_id + + +@pytest.mark.asyncio +async def test_fetch_reset_credits_normalizes_base_url_without_backend_api_segment() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(200, {"credits": [], "available_count": 0}, "")], state) + + await fetch_reset_credits( + "access-token", + None, + base_url="http://upstream.test/", # trailing slash, no backend-api + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert state.url == "http://upstream.test/backend-api/wham/rate-limit-reset-credits" + + +@pytest.mark.asyncio +async def test_fetch_reset_credits_raises_on_non_200() -> None: + state = ClientState() + client = StubRetryClient( + [StubResponse(401, {"error": {"code": "unauthorized", "message": "bad token"}}, "")], + state, + ) + + with pytest.raises(ResetCreditFetchError) as excinfo: + await fetch_reset_credits( + "access-token", + None, + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 401 + assert excinfo.value.code == "unauthorized" + + +@pytest.mark.asyncio +async def test_fetch_reset_credits_handles_non_json_body() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(502, None, "boom")], state) + + with pytest.raises(ResetCreditFetchError) as excinfo: + await fetch_reset_credits( + "access-token", + None, + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 502 + + +@pytest.mark.asyncio +async def test_consume_reset_credit_sends_credit_id_and_uuid_redeem_request_id() -> None: + state = ClientState() + client = StubRetryClient( + [ + StubResponse( + 200, + { + "code": "reset", + "credit": { + "id": "RateLimitResetCredit_test", + "status": "redeemed", + "redeemed_at": "2026-06-13T13:12:31Z", + }, + "windows_reset": 1, + }, + "", + ) + ], + state, + ) + + result = await consume_reset_credit( + "access-token", + "acc_workspace", + "RateLimitResetCredit_test", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert isinstance(result, ConsumeResetCreditResponse) + assert result.code == "reset" + assert result.windows_reset == 1 + assert result.credit is not None and result.credit.redeemed_at is not None + assert state.method == "POST" + assert state.url == "http://upstream.test/backend-api/wham/rate-limit-reset-credits/consume" + assert state.headers is not None + assert state.headers["Authorization"] == "Bearer access-token" + assert state.headers["chatgpt-account-id"] == "acc_workspace" + assert state.headers["Content-Type"] == "application/json" + # body carries the credit id and a freshly-generated uuid redeem_request_id + assert state.json_body is not None + assert state.json_body["credit_id"] == "RateLimitResetCredit_test" + redeem_request_id = state.json_body["redeem_request_id"] + assert isinstance(redeem_request_id, str) and len(redeem_request_id) == 36 + # canonical uuid v4 + parsed = UUID(redeem_request_id, version=4) + assert str(parsed) == redeem_request_id + + +@pytest.mark.asyncio +async def test_consume_reset_credit_generates_fresh_redeem_request_id_each_call() -> None: + ids: list[str] = [] + for _ in range(2): + state = ClientState() + client = StubRetryClient( + [StubResponse(200, {"code": "reset", "credit": {"id": "x"}, "windows_reset": 1}, "")], + state, + ) + await consume_reset_credit( + "access-token", + None, + "RateLimitResetCredit_test", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + assert state.json_body is not None + ids.append(state.json_body["redeem_request_id"]) + assert ids[0] != ids[1] + + +@pytest.mark.asyncio +async def test_consume_reset_credit_raises_on_non_200() -> None: + state = ClientState() + client = StubRetryClient( + [StubResponse(409, {"error": {"code": "no_credit", "message": "none"}}, "")], + state, + ) + + with pytest.raises(ConsumeResetCreditError) as excinfo: + await consume_reset_credit( + "access-token", + None, + "RateLimitResetCredit_test", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 409 + assert excinfo.value.code == "no_credit" + + +def test_build_snapshot_projects_nearest_available_expiry() -> None: + response = ResetCreditsResponse.model_validate( + { + "credits": [ + {"id": "a", "status": "available", "expires_at": "2026-07-10T00:00:00Z"}, + {"id": "b", "status": "available", "expires_at": "2026-06-20T00:00:00Z"}, + {"id": "c", "status": "redeemed", "expires_at": "2026-06-01T00:00:00Z"}, + ], + "available_count": 2, + } + ) + + snapshot = build_snapshot(response) + + assert snapshot.available_count == 2 + assert snapshot.nearest_expires_at is not None + assert snapshot.nearest_expires_at.year == 2026 + assert snapshot.nearest_expires_at.month == 6 + assert snapshot.nearest_expires_at.day == 20 + assert [credit.id for credit in snapshot.credits] == ["a", "b", "c"] + + +def test_build_snapshot_returns_none_expiry_when_no_available_credit() -> None: + response = ResetCreditsResponse.model_validate( + {"credits": [{"id": "a", "status": "redeemed"}], "available_count": 0} + ) + assert build_snapshot(response).nearest_expires_at is None diff --git a/tests/unit/test_rate_limit_reset_credits_mapper.py b/tests/unit/test_rate_limit_reset_credits_mapper.py new file mode 100644 index 000000000..8774bc48f --- /dev/null +++ b/tests/unit/test_rate_limit_reset_credits_mapper.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from datetime import datetime + +import pytest + +from app.core.clients.rate_limit_reset_credits import RateLimitResetCreditsSnapshot +from app.core.crypto import TokenEncryptor +from app.db.models import Account, AccountStatus +from app.modules.accounts.mappers import build_account_summaries +from app.modules.rate_limit_reset_credits.store import RateLimitResetCreditsStore + +pytestmark = pytest.mark.unit + + +def _account(account_id: str) -> Account: + return Account( + id=account_id, + chatgpt_account_id=f"workspace-{account_id}", + email=f"{account_id}@example.com", + plan_type="plus", + access_token_encrypted=b"", + refresh_token_encrypted=b"", + id_token_encrypted=b"", + last_refresh=datetime(2025, 1, 1), + status=AccountStatus.ACTIVE, + ) + + +def _summaries(accounts: list[Account], store: RateLimitResetCreditsStore): + return build_account_summaries( + accounts=accounts, + primary_usage={}, + secondary_usage={}, + encryptor=TokenEncryptor(), + include_auth=False, + reset_credits_store=store, + ) + + +def test_account_summary_exposes_cached_reset_credits_fields() -> None: + store = RateLimitResetCreditsStore() + nearest = datetime(2026, 7, 10, 0, 0, 0) + store_snapshot = RateLimitResetCreditsSnapshot( + available_count=2, + nearest_expires_at=nearest, + credits=[], + ) + # Bypass the async lock by writing the backing dict directly for a unit fixture. + store._snapshots["acc_with_credits"] = store_snapshot # type: ignore[attr-defined] + + [summary] = _summaries([_account("acc_with_credits")], store) + + assert summary.available_reset_credits == 2 + assert summary.reset_credit_nearest_expires_at == nearest + + +def test_account_summary_returns_zero_and_null_when_no_snapshot() -> None: + store = RateLimitResetCreditsStore() + + [summary] = _summaries([_account("acc_no_cache")], store) + + assert summary.available_reset_credits == 0 + assert summary.reset_credit_nearest_expires_at is None + + +def test_account_summary_mixed_cache_state_across_accounts() -> None: + store = RateLimitResetCreditsStore() + store._snapshots["acc_has"] = RateLimitResetCreditsSnapshot( # type: ignore[attr-defined] + available_count=5, + nearest_expires_at=datetime(2026, 6, 20, 0, 0, 0), + credits=[], + ) + + summaries = _summaries([_account("acc_has"), _account("acc_missing")], store) + by_id = {s.account_id: s for s in summaries} + + assert by_id["acc_has"].available_reset_credits == 5 + assert by_id["acc_has"].reset_credit_nearest_expires_at is not None + assert by_id["acc_missing"].available_reset_credits == 0 + assert by_id["acc_missing"].reset_credit_nearest_expires_at is None + + +def test_account_summary_does_not_crash_when_store_is_empty() -> None: + store = RateLimitResetCreditsStore() + accounts = [_account(f"acc_{i}") for i in range(3)] + + summaries = _summaries(accounts, store) + + assert len(summaries) == 3 + assert all(s.available_reset_credits == 0 for s in summaries) + assert all(s.reset_credit_nearest_expires_at is None for s in summaries) diff --git a/tests/unit/test_rate_limit_reset_credits_scheduler.py b/tests/unit/test_rate_limit_reset_credits_scheduler.py new file mode 100644 index 000000000..4e71314b8 --- /dev/null +++ b/tests/unit/test_rate_limit_reset_credits_scheduler.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager +from datetime import datetime +from typing import Any + +import pytest + +from app.core.clients.rate_limit_reset_credits import ( + RateLimitResetCreditsSnapshot, + ResetCreditFetchError, + ResetCreditsResponse, +) +from app.core.crypto import TokenEncryptor +from app.core.usage import reset_credits_refresh_scheduler as scheduler_module +from app.core.usage.reset_credits_refresh_scheduler import ( + RateLimitResetCreditsRefreshScheduler, + refresh_reset_credits_for_accounts, +) +from app.db.models import Account, AccountStatus +from app.modules.rate_limit_reset_credits.store import RateLimitResetCreditsStore + +pytestmark = pytest.mark.unit + + +class StubEncryptor(TokenEncryptor): + def __init__(self) -> None: + # Skip key-file I/O; tests only exercise decrypt(). + pass + + def decrypt(self, encrypted: bytes) -> str: + return f"token-for-{encrypted.decode() if encrypted else ''}" + + +def _make_account( + account_id: str, + *, + status: AccountStatus = AccountStatus.ACTIVE, + chatgpt_account_id: str | None = "workspace-x", +) -> Account: + return Account( + id=account_id, + chatgpt_account_id=chatgpt_account_id, + email=f"{account_id}@example.com", + plan_type="plus", + access_token_encrypted=account_id.encode(), + refresh_token_encrypted=b"refresh", + id_token_encrypted=b"id", + last_refresh=datetime(2025, 1, 1), + status=status, + ) + + +def _response(available_count: int = 1) -> ResetCreditsResponse: + return ResetCreditsResponse.model_validate( + { + "credits": [ + {"id": "c1", "status": "available", "expires_at": "2026-07-12T00:00:00Z"}, + ], + "available_count": available_count, + } + ) + + +@pytest.mark.asyncio +async def test_refresh_skips_paused_and_deactivated_accounts() -> None: + store = RateLimitResetCreditsStore() + fetched: list[str] = [] + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + fetched.append(access_token) + return _response() + + accounts = [ + _make_account("acc_paused", status=AccountStatus.PAUSED), + _make_account("acc_deactivated", status=AccountStatus.DEACTIVATED), + _make_account("acc_active"), + ] + + await refresh_reset_credits_for_accounts( + accounts=accounts, + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + # Only the active account was fetched and cached. + assert fetched == ["token-for-acc_active"] + assert store.get("acc_paused") is None + assert store.get("acc_deactivated") is None + assert store.get("acc_active") is not None + + +@pytest.mark.asyncio +async def test_refresh_skips_account_without_chatgpt_account_id() -> None: + store = RateLimitResetCreditsStore() + fetched: list[str] = [] + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + fetched.append(access_token) + return _response() + + await refresh_reset_credits_for_accounts( + accounts=[_make_account("acc_no_workspace", chatgpt_account_id=None)], + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + assert fetched == [] + assert store.get("acc_no_workspace") is None + + +@pytest.mark.asyncio +async def test_one_account_failure_does_not_break_the_loop() -> None: + store = RateLimitResetCreditsStore() + fetched: list[str] = [] + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + fetched.append(access_token) + if access_token == "token-for-acc_fail": + raise ResetCreditFetchError(500, "boom") + return _response(available_count=3) + + accounts = [_make_account("acc_fail"), _make_account("acc_ok")] + + await refresh_reset_credits_for_accounts( + accounts=accounts, + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + # Both accounts were attempted despite the first raising. + assert fetched == ["token-for-acc_fail", "token-for-acc_ok"] + # The failing account left no snapshot; the healthy one was cached. + assert store.get("acc_fail") is None + ok_snapshot = store.get("acc_ok") + assert ok_snapshot is not None + assert ok_snapshot.available_count == 3 + + +@pytest.mark.asyncio +async def test_upstream_error_retains_prior_snapshot_and_does_not_mutate_status() -> None: + store = RateLimitResetCreditsStore() + prior = RateLimitResetCreditsSnapshot(available_count=2) + await store.set("acc_retain", prior) + account = _make_account("acc_retain", status=AccountStatus.ACTIVE) + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + raise ResetCreditFetchError(503, "busy") + + await refresh_reset_credits_for_accounts( + accounts=[account], + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + # Prior snapshot is retained exactly. + assert store.get("acc_retain") is prior + assert prior.available_count == 2 + # Account status is untouched. + assert account.status == AccountStatus.ACTIVE + + +@pytest.mark.asyncio +async def test_refresh_never_calls_account_status_writes() -> None: + """The scheduler must not transition account status under any path. + + The refresh function operates only on the in-memory store; it holds no + reference to a repository and therefore cannot perform status writes. We + assert the account objects are byte-identical in status before and after, + including across the failure path. + """ + store = RateLimitResetCreditsStore() + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + if access_token == "token-for-acc_fail": + raise ResetCreditFetchError(401, "unauthorized") + return _response() + + accounts = [_make_account("acc_fail"), _make_account("acc_ok")] + statuses_before = {a.id: a.status for a in accounts} + + await refresh_reset_credits_for_accounts( + accounts=accounts, + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + assert {a.id: a.status for a in accounts} == statuses_before + + +@pytest.mark.asyncio +async def test_refresh_once_skips_when_not_leader(monkeypatch: pytest.MonkeyPatch) -> None: + """Non-leader replicas perform no upstream fetches and open no DB session.""" + + class NonLeader: + async def try_acquire(self) -> bool: + return False + + monkeypatch.setattr(scheduler_module, "_get_leader_election", lambda: NonLeader()) + + session_entered = False + + @asynccontextmanager + async def _forbidden_session(): # type: ignore[no-untyped-def] + nonlocal session_entered + session_entered = True + yield None + + monkeypatch.setattr(scheduler_module, "get_background_session", _forbidden_session) + + scheduler = RateLimitResetCreditsRefreshScheduler(interval_seconds=60, enabled=True) + await scheduler._refresh_once() + + assert session_entered is False + + +@pytest.mark.asyncio +async def test_refresh_once_leader_path_caches_snapshots(monkeypatch: pytest.MonkeyPatch) -> None: + """End-to-end leader-gated tick wires accounts -> store without status writes.""" + + class Leader: + async def try_acquire(self) -> bool: + return True + + monkeypatch.setattr(scheduler_module, "_get_leader_election", lambda: Leader()) + + account = _make_account("acc_leader") + store = RateLimitResetCreditsStore() + + captured: list[Any] = [] + + class _FakeRepo: + async def list_accounts(self) -> list[Account]: + captured.append("list_accounts") + return [account] + + class _FakeSession: + async def __aenter__(self) -> "_FakeSession": + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + return None + + @asynccontextmanager + async def _fake_background_session(): + captured.append("session_opened") + yield _FakeSession() + + monkeypatch.setattr(scheduler_module, "get_background_session", _fake_background_session) + monkeypatch.setattr(scheduler_module, "AccountsRepository", lambda session: _FakeRepo()) + monkeypatch.setattr(scheduler_module, "TokenEncryptor", lambda: StubEncryptor()) + monkeypatch.setattr(scheduler_module, "get_rate_limit_reset_credits_store", lambda: store) + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + captured.append(("fetch", access_token, account_id)) + return _response(available_count=7) + + monkeypatch.setattr(scheduler_module, "fetch_reset_credits", fetch_fn) + + scheduler = RateLimitResetCreditsRefreshScheduler(interval_seconds=60, enabled=True) + await scheduler._refresh_once() + + assert ("fetch", "token-for-acc_leader", "workspace-x") in captured + leader_snapshot = store.get("acc_leader") + assert leader_snapshot is not None + assert leader_snapshot.available_count == 7 + assert account.status == AccountStatus.ACTIVE diff --git a/tests/unit/test_rate_limit_reset_credits_store.py b/tests/unit/test_rate_limit_reset_credits_store.py new file mode 100644 index 000000000..1aa90fd05 --- /dev/null +++ b/tests/unit/test_rate_limit_reset_credits_store.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import pytest + +from app.core.clients.rate_limit_reset_credits import RateLimitResetCreditsSnapshot +from app.modules.rate_limit_reset_credits.store import ( + RateLimitResetCreditsStore, + get_rate_limit_reset_credits_store, +) + +pytestmark = pytest.mark.unit + + +def _snapshot(available_count: int = 1) -> RateLimitResetCreditsSnapshot: + return RateLimitResetCreditsSnapshot(available_count=available_count) + + +@pytest.mark.asyncio +async def test_set_and_get_round_trip() -> None: + store = RateLimitResetCreditsStore() + snapshot = _snapshot(2) + + await store.set("acc_a", snapshot) + + assert store.get("acc_a") is snapshot + assert snapshot.available_count == 2 + + +@pytest.mark.asyncio +async def test_get_returns_none_for_missing_account() -> None: + store = RateLimitResetCreditsStore() + assert store.get("missing") is None + + +@pytest.mark.asyncio +async def test_set_overwrites_prior_snapshot() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_a", _snapshot(1)) + await store.set("acc_a", _snapshot(5)) + + snapshot = store.get("acc_a") + assert snapshot is not None + assert snapshot.available_count == 5 + + +@pytest.mark.asyncio +async def test_invalidate_single_account_clears_only_that_key() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_a", _snapshot(1)) + await store.set("acc_b", _snapshot(2)) + + await store.invalidate("acc_a") + + assert store.get("acc_a") is None + snapshot_b = store.get("acc_b") + assert snapshot_b is not None + assert snapshot_b.available_count == 2 + + +@pytest.mark.asyncio +async def test_invalidate_all_clears_every_key() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_a", _snapshot(1)) + await store.set("acc_b", _snapshot(2)) + + await store.invalidate() + + assert store.get("acc_a") is None + assert store.get("acc_b") is None + + +@pytest.mark.asyncio +async def test_invalidate_missing_account_is_noop() -> None: + store = RateLimitResetCreditsStore() + await store.invalidate("never_existed") # must not raise + assert store.get("never_existed") is None + + +@pytest.mark.asyncio +async def test_concurrent_setters_are_serialized_under_lock() -> None: + store = RateLimitResetCreditsStore() + + async def writer(account_id: str) -> None: + for value in range(20): + await store.set(account_id, _snapshot(value)) + + # If the lock did not serialize, a careless implementation could still pass, + # but a dict is not coroutine-safe across truly concurrent writes; this at + # least exercises the lock path and confirms the final state is consistent. + import asyncio + + await asyncio.gather(*(writer(f"acc_{i}") for i in range(5))) + + for i in range(5): + snapshot = store.get(f"acc_{i}") + assert snapshot is not None + assert snapshot.available_count == 19 + + +def test_module_singleton_accessor_returns_shared_instance() -> None: + assert get_rate_limit_reset_credits_store() is get_rate_limit_reset_credits_store() From a388a659bd60ba5f1b5c42711a37ef0917aca70a Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Thu, 18 Jun 2026 02:38:30 +0800 Subject: [PATCH 02/39] ui adjustments --- app/core/config/settings.py | 1 - .../usage/reset_credits_refresh_scheduler.py | 4 -- .../src/components/layout/app-header.test.tsx | 64 +++++++++++++++++++ frontend/src/components/layout/app-header.tsx | 35 +++++++++- .../components/account-actions.test.tsx | 4 +- .../accounts/components/account-actions.tsx | 5 +- .../reset-credit-confirm-dialog.test.tsx | 2 +- .../reset-credit-confirm-dialog.tsx | 4 +- .../components/account-card.test.tsx | 4 +- .../dashboard/components/account-card.tsx | 5 +- .../dashboard/components/account-list.tsx | 17 ++--- frontend/src/utils/formatters.test.ts | 10 +++ frontend/src/utils/formatters.ts | 12 ++++ .../add-rate-limit-reset-credits/design.md | 9 +-- .../add-rate-limit-reset-credits/proposal.md | 12 ++-- .../specs/frontend-architecture/spec.md | 28 ++++++-- .../specs/rate-limit-reset-credits/context.md | 5 +- .../specs/rate-limit-reset-credits/spec.md | 15 ++--- .../add-rate-limit-reset-credits/tasks.md | 16 +++-- tests/conftest.py | 17 ++++- ...test_rate_limit_reset_credits_scheduler.py | 4 +- 21 files changed, 213 insertions(+), 60 deletions(-) create mode 100644 frontend/src/components/layout/app-header.test.tsx diff --git a/app/core/config/settings.py b/app/core/config/settings.py index 3c854f783..53b7b3689 100644 --- a/app/core/config/settings.py +++ b/app/core/config/settings.py @@ -194,7 +194,6 @@ class Settings(BaseSettings): usage_fetch_max_retries: int = 2 usage_refresh_enabled: bool = True usage_refresh_interval_seconds: int = Field(default=60, gt=0) - rate_limit_reset_credits_refresh_enabled: bool = True rate_limit_reset_credits_refresh_interval_seconds: int = Field(default=60, gt=0) openai_cache_affinity_max_age_seconds: int = Field(default=1800, gt=0) warmup_model: str = "gpt-5.4-mini" diff --git a/app/core/usage/reset_credits_refresh_scheduler.py b/app/core/usage/reset_credits_refresh_scheduler.py index f470e1c01..99e7702fd 100644 --- a/app/core/usage/reset_credits_refresh_scheduler.py +++ b/app/core/usage/reset_credits_refresh_scheduler.py @@ -42,14 +42,11 @@ def _get_leader_election() -> _LeaderElectionLike: @dataclass(slots=True) class RateLimitResetCreditsRefreshScheduler: interval_seconds: int - enabled: bool _task: asyncio.Task[None] | None = None _stop: asyncio.Event = field(default_factory=asyncio.Event) _lock: asyncio.Lock = field(default_factory=asyncio.Lock) async def start(self) -> None: - if not self.enabled: - return if self._task and not self._task.done(): return self._stop.clear() @@ -137,5 +134,4 @@ def build_rate_limit_reset_credits_scheduler() -> RateLimitResetCreditsRefreshSc settings = get_settings() return RateLimitResetCreditsRefreshScheduler( interval_seconds=settings.rate_limit_reset_credits_refresh_interval_seconds, - enabled=settings.rate_limit_reset_credits_refresh_enabled, ) diff --git a/frontend/src/components/layout/app-header.test.tsx b/frontend/src/components/layout/app-header.test.tsx new file mode 100644 index 000000000..5c45a3c2c --- /dev/null +++ b/frontend/src/components/layout/app-header.test.tsx @@ -0,0 +1,64 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import { HttpResponse, http } from "msw"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; + +import { AppHeader } from "@/components/layout/app-header"; +import { server } from "@/test/mocks/server"; +import { createAccountSummary } from "@/test/mocks/factories"; + +function renderHeader() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return render( + + + + + , + ); +} + +describe("AppHeader", () => { + it("shows the summed Accounts reset-credit badge capped at 99+", async () => { + server.use( + http.get("/api/accounts", () => + HttpResponse.json({ + accounts: [ + createAccountSummary({ availableResetCredits: 70 }), + createAccountSummary({ accountId: "acc-2", availableResetCredits: 40 }), + ], + }), + ), + ); + + renderHeader(); + + expect(await screen.findAllByText("99+")).not.toHaveLength(0); + }); + + it("hides the Accounts reset-credit badge when no resets are available", async () => { + server.use( + http.get("/api/accounts", () => + HttpResponse.json({ + accounts: [ + createAccountSummary({ availableResetCredits: 0 }), + createAccountSummary({ accountId: "acc-2", availableResetCredits: 0 }), + ], + }), + ), + ); + + renderHeader(); + + await screen.findByRole("link", { name: /Accounts/i }); + expect(screen.queryByText("99+")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/layout/app-header.tsx b/frontend/src/components/layout/app-header.tsx index b83a6cd48..1c5eb4f25 100644 --- a/frontend/src/components/layout/app-header.tsx +++ b/frontend/src/components/layout/app-header.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@tanstack/react-query"; import { Eye, EyeOff, LogIn, LogOut, Menu } from "lucide-react"; import { useState } from "react"; import { NavLink } from "react-router-dom"; @@ -5,6 +6,7 @@ import { NavLink } from "react-router-dom"; import { CodexLogo } from "@/components/brand/codex-logo"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { listAccounts } from "@/features/accounts/api"; import { usePrivacyStore } from "@/hooks/use-privacy"; import { cn } from "@/lib/utils"; @@ -35,6 +37,23 @@ export function AppHeader({ const blurred = usePrivacyStore((s) => s.blurred); const togglePrivacy = usePrivacyStore((s) => s.toggle); const PrivacyIcon = blurred ? EyeOff : Eye; + const { data: accounts = [] } = useQuery({ + queryKey: ["accounts", "list"], + queryFn: listAccounts, + select: (data) => data.accounts, + refetchInterval: 30_000, + refetchIntervalInBackground: false, + staleTime: 30_000, + }); + const totalAvailableResetCredits = accounts.reduce( + (total, account) => total + Math.max(0, account.availableResetCredits ?? 0), + 0, + ); + const accountsResetBadge = totalAvailableResetCredits > 99 + ? "99+" + : totalAvailableResetCredits > 0 + ? String(totalAvailableResetCredits) + : null; return (
- {item.label} + + {item.label} + {item.to === "/accounts" && accountsResetBadge ? ( + + {accountsResetBadge} + + ) : null} + ))} @@ -133,13 +159,18 @@ export function AppHeader({ {({ isActive }) => ( {item.label} + {item.to === "/accounts" && accountsResetBadge ? ( + + {accountsResetBadge} + + ) : null} )} diff --git a/frontend/src/features/accounts/components/account-actions.test.tsx b/frontend/src/features/accounts/components/account-actions.test.tsx index b432b65fb..b08f8c1b9 100644 --- a/frontend/src/features/accounts/components/account-actions.test.tsx +++ b/frontend/src/features/accounts/components/account-actions.test.tsx @@ -182,7 +182,7 @@ describe("AccountActions", () => { />, ); - await user.click(screen.getByRole("button", { name: "Reset" })); + await user.click(screen.getByRole("button", { name: "Reset (3)" })); expect(onResetCredit).toHaveBeenCalledWith(account.accountId); }); @@ -210,6 +210,6 @@ describe("AccountActions", () => { />, ); - expect(screen.queryByRole("button", { name: "Reset" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /Reset \(/ })).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/features/accounts/components/account-actions.tsx b/frontend/src/features/accounts/components/account-actions.tsx index 5a6e94294..ab096ed61 100644 --- a/frontend/src/features/accounts/components/account-actions.tsx +++ b/frontend/src/features/accounts/components/account-actions.tsx @@ -67,7 +67,8 @@ export function AccountActions({ const resetCountdown = account.resetCreditNearestExpiresAt ? formatSingleUnitRemaining(account.resetCreditNearestExpiresAt) : null; - const hasResetCredits = (account.availableResetCredits ?? 0) > 0; + const availableResetCredits = account.availableResetCredits ?? 0; + const hasResetCredits = availableResetCredits > 0; return (
@@ -209,7 +210,7 @@ export function AccountActions({ disabled={busy || readOnly} > - Reset + {`Reset (${availableResetCredits})`} {resetCountdown ? (

diff --git a/frontend/src/features/accounts/schemas.test.ts b/frontend/src/features/accounts/schemas.test.ts index e98a105e9..014586d91 100644 --- a/frontend/src/features/accounts/schemas.test.ts +++ b/frontend/src/features/accounts/schemas.test.ts @@ -4,8 +4,10 @@ import { AccountAuthExportResponseSchema, AccountProbeResponseSchema, AccountSummarySchema, + ConsumeRateLimitResetCreditResponseSchema, ImportStateSchema, OAuthStateSchema, + RateLimitResetCreditsSnapshotSchema, } from "@/features/accounts/schemas"; const ISO = "2026-01-01T00:00:00+00:00"; @@ -187,3 +189,50 @@ describe("AccountProbeResponseSchema", () => { expect(parsed.accountId).toBe("acc-1"); }); }); + +describe("RateLimitResetCreditsSnapshotSchema", () => { + it("parses reset-credit items when nullable backend fields are omitted or null", () => { + const parsed = RateLimitResetCreditsSnapshotSchema.parse({ + availableCount: 2, + nearestExpiresAt: null, + credits: [ + { + id: "credit-1", + expiresAt: ISO, + }, + { + id: "credit-2", + status: null, + expiresAt: null, + }, + ], + }); + + expect(parsed.credits[0]?.status).toBeUndefined(); + expect(parsed.credits[1]?.status).toBeNull(); + }); +}); + +describe("ConsumeRateLimitResetCreditResponseSchema", () => { + it("parses consume responses when nullable backend fields are omitted or null", () => { + expect( + ConsumeRateLimitResetCreditResponseSchema.parse({ + redeemedAt: ISO, + }), + ).toMatchObject({ + redeemedAt: ISO, + }); + + expect( + ConsumeRateLimitResetCreditResponseSchema.parse({ + code: null, + windowsReset: null, + redeemedAt: null, + }), + ).toMatchObject({ + code: null, + windowsReset: null, + redeemedAt: null, + }); + }); +}); diff --git a/frontend/src/features/accounts/schemas.ts b/frontend/src/features/accounts/schemas.ts index eadcddfba..378b9cca3 100644 --- a/frontend/src/features/accounts/schemas.ts +++ b/frontend/src/features/accounts/schemas.ts @@ -97,7 +97,7 @@ export const AccountSummarySchema = z.object({ const RateLimitResetCreditItemSchema = z.object({ id: z.string(), - status: z.string(), + status: z.string().nullable().optional(), resetType: z.string().nullable().optional(), grantedAt: z.iso.datetime({ offset: true }).nullable().optional(), expiresAt: z.iso.datetime({ offset: true }).nullable().optional(), @@ -114,8 +114,8 @@ export const RateLimitResetCreditsSnapshotSchema = z.object({ }); export const ConsumeRateLimitResetCreditResponseSchema = z.object({ - code: z.string(), - windowsReset: z.number(), + code: z.string().nullable().optional(), + windowsReset: z.number().nullable().optional(), redeemedAt: z.iso.datetime({ offset: true }).nullable(), }); diff --git a/frontend/src/features/accounts/sorting.test.ts b/frontend/src/features/accounts/sorting.test.ts index 79ccbc584..27501faca 100644 --- a/frontend/src/features/accounts/sorting.test.ts +++ b/frontend/src/features/accounts/sorting.test.ts @@ -1,12 +1,16 @@ import { describe, expect, it } from "vitest"; -import { sortAccountsForDisplay } from "@/features/accounts/sorting"; +import { DEFAULT_ACCOUNT_SORT_MODE, sortAccountsForDisplay } from "@/features/accounts/sorting"; import type { AccountSummary } from "@/features/accounts/schemas"; import { createAccountSummary } from "@/test/mocks/factories"; const BOTH = "both"; describe("sortAccountsForDisplay — most_reset_credits", () => { + it("uses most reset credits as the default sort mode", () => { + expect(DEFAULT_ACCOUNT_SORT_MODE).toBe("most_reset_credits"); + }); + it("orders accounts by available reset credits descending", () => { const fewer = createAccountSummary({ accountId: "acc-fewer", diff --git a/frontend/src/features/accounts/sorting.ts b/frontend/src/features/accounts/sorting.ts index 4d42e5b14..ab932ffb1 100644 --- a/frontend/src/features/accounts/sorting.ts +++ b/frontend/src/features/accounts/sorting.ts @@ -17,7 +17,7 @@ export const ACCOUNT_SORT_OPTIONS: readonly { value: AccountSortMode; label: str { value: "name_desc", label: "Name (Z-A)" }, ] as const; -export const DEFAULT_ACCOUNT_SORT_MODE: AccountSortMode = "reset_soonest"; +export const DEFAULT_ACCOUNT_SORT_MODE: AccountSortMode = "most_reset_credits"; function visibleQuotaResetTimestamps( account: AccountSummary, diff --git a/openspec/changes/add-rate-limit-reset-credits/design.md b/openspec/changes/add-rate-limit-reset-credits/design.md index 78d12a8a9..24842bf98 100644 --- a/openspec/changes/add-rate-limit-reset-credits/design.md +++ b/openspec/changes/add-rate-limit-reset-credits/design.md @@ -30,7 +30,7 @@ The reference is a single-account CLI; codex-lb needs a multi-account, dashboard ## Decisions ### Decision: Dedicated module + scheduler, mirroring `usage_refresh` (not folding into the usage loop) -**Rationale:** Usage refresh owns account-status derivation and has a dense, scenario-heavy spec (`usage-refresh-policy`) tying it to quota reconciliation, cooldowns, and warm-up. Bolting credits onto that loop would couple two upstream calls and their failure modes, and muddy the usage-refresh contract. A dedicated `RateLimitResetCreditsRefreshScheduler` reuses the exact `UsageRefreshScheduler` shape (leader-gated, `asyncio.Lock`-guarded `_refresh_once`, interval-only configuration) and always starts with the application. +**Rationale:** Usage refresh owns account-status derivation and has a dense, scenario-heavy spec (`usage-refresh-policy`) tying it to quota reconciliation, cooldowns, and warm-up. Bolting credits onto that loop would couple two upstream calls and their failure modes, and muddy the usage-refresh contract. A dedicated `RateLimitResetCreditsRefreshScheduler` reuses the `UsageRefreshScheduler` loop shape (`asyncio.Lock`-guarded `_refresh_once`, interval-only configuration) and always starts with the application. Unlike usage refresh, it deliberately runs on every replica because reset-credit snapshots are process-local and dashboard reads must be consistent regardless of which replica handles the request. **Alternatives considered:** (a) Fold into `UsageRefreshScheduler._refresh_once` — rejected for the coupling above. (b) Pure passthrough via the local `wham_router` proxy — rejected because the dashboard needs the in-memory store and per-account token decryption that the proxy router does not have, and the requirement is "refresh every 60s + store in-memory." ### Decision: Server picks the soonest-expiring credit at consume time @@ -55,7 +55,7 @@ The reference is a single-account CLI; codex-lb needs a multi-account, dashboard - **[In-memory cache lost on restart]** → Mitigation: acceptable per requirements; the next 60s tick repopulates. UI treats missing snapshot as `available_reset_credits: 0` (hidden affordances), not an error. - **[Credit consumed even on partial reset]** (upstream behavior: "if POST returns 200, the credit is gone") → Mitigation: confirmation dialog explicitly warns that the credit is consumed regardless of whether the rate-limit window moves. On success we invalidate the cache and let the next tick reconcile. - **[Race: credit expires between render and click]** → Mitigation: server re-selects from the freshest cached snapshot at consume time and surfaces upstream's error if the chosen credit is no longer redeemable. -- **[Many accounts = many upstream calls per tick]** → Mitigation: existing per-account usage-refresh already pays this cost on the same cadence; leader-gated so only one replica polls. Reuses the same skip rules (paused/deactivated/missing chatgpt-account-id). +- **[Many accounts = many upstream calls per tick]** → Mitigation: reuse the same skip rules (paused/deactivated/missing chatgpt-account-id) and keep the interval configurable. Each replica polls so its process-local cache is useful for dashboard reads; moving snapshots to shared storage can later reduce duplicate polling if upstream load becomes a problem. - **[Guest read-only dashboard users]** → Mitigation: `POST /consume` requires `require_dashboard_write_access`; guests can see the badge/button (count is read off `AccountSummary`) but cannot redeem. ## Migration Plan diff --git a/openspec/changes/add-rate-limit-reset-credits/proposal.md b/openspec/changes/add-rate-limit-reset-credits/proposal.md index 7321b2e3f..d85f70a35 100644 --- a/openspec/changes/add-rate-limit-reset-credits/proposal.md +++ b/openspec/changes/add-rate-limit-reset-credits/proposal.md @@ -24,7 +24,7 @@ OpenAI rolled out savable ("banked") rate-limit reset credits for Codex on 2026- ## Impact -- **Backend (new)**: `app/core/clients/rate_limit_reset_credits.py` (upstream client), `app/modules/rate_limit_reset_credits/store.py` (in-memory store + singleton), `app/core/usage/reset_credits_refresh_scheduler.py` (60s leader-gated loop), `app/modules/rate_limit_reset_credits/api.py` (dashboard GET + consume POST). +- **Backend (new)**: `app/core/clients/rate_limit_reset_credits.py` (upstream client), `app/modules/rate_limit_reset_credits/store.py` (in-memory store + singleton), `app/core/usage/reset_credits_refresh_scheduler.py` (60s per-replica loop), `app/modules/rate_limit_reset_credits/api.py` (dashboard GET + consume POST). - **Backend (modified)**: `app/main.py` lifespan wiring (build/start/stop the new scheduler); `app/core/config/settings.py` (refresh interval setting only); account-summary mappers in `app/modules/accounts/` and the dashboard mapper to join the cached snapshot onto `AccountSummary`. - **Frontend (new/modified)**: `features/accounts/schemas.ts` + `features/dashboard/schemas.ts` (two new fields); `features/accounts/api.ts` (consume client); `features/accounts/sorting.ts` (new sort mode); `features/accounts/components/account-actions.tsx` (Reset button); `features/accounts/components/account-list-item.tsx` (count badge); `features/dashboard/components/account-list.tsx` + `account-card.tsx` (Reset button); `components/layout/app-header.tsx` (Accounts total badge); `utils/formatters.ts` (single-unit countdown + local expiry timestamp formatter); reuse of `components/confirm-dialog.tsx` + `hooks/use-dialog-state.ts` for the confirmation flow. - **Upstream contract**: undocumented OpenAI endpoints under `https://chatgpt.com/backend-api/wham/rate-limit-reset-credits`; behavior is best-effort and may change upstream (see context doc). diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md index 6aa6c9a73..836f4b0ce 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md @@ -40,10 +40,10 @@ The Accounts page `AccountListItem` SHALL render a count badge pinned to the rig ### Requirement: Accounts page can sort by available reset credits -The Accounts page sort selector SHALL offer a "Most reset credits" option that orders accounts by `available_reset_credits` descending. Ties SHALL be broken by `reset_credit_nearest_expires_at` ascending (soonest expiring first), and accounts with no expiry SHALL sort after accounts that have one. +The Accounts page sort selector SHALL offer a "Most reset credits" option and SHALL use it as the default Accounts page ordering. That ordering SHALL sort accounts by `available_reset_credits` descending. Ties SHALL be broken by `reset_credit_nearest_expires_at` ascending (soonest expiring first), and accounts with no expiry SHALL sort after accounts that have one. #### Scenario: More available credits sorts first -- **WHEN** the operator selects "Most reset credits" +- **WHEN** the operator opens the Accounts page with the default sort mode - **AND** account A has `available_reset_credits: 4` and account B has `available_reset_credits: 1` - **THEN** account A appears before account B diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md index 321a6b906..f7f1e3465 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md @@ -25,22 +25,23 @@ These endpoints are undocumented and were reverse-engineered from the official [`aaamosh/codex-reset`](https://github.com/aaamosh/codex-reset) — a single-account CLI implementation that codex-lb's multi-account, dashboard-driven, in-memory-cached variant is based on. OpenAI may rename, gate, or remove these endpoints at any time; the codex-lb -client treats non-200/non-JSON responses defensively. +client treats non-200, non-JSON, and schema-drifted 200 responses defensively. ## Decisions -- **In-memory only.** No DB column, no migration. Snapshots repopulate within one tick of - startup. Restart cost: up to 60s of `available_reset_credits: 0` everywhere. +- **In-memory only.** No DB column, no migration. Each replica refreshes its own process-local + snapshots, which repopulate within one tick of startup. Restart cost: up to 60s of + `available_reset_credits: 0` on that replica. - **Server picks the credit, not the client.** `POST /consume` takes only the account id; the server selects the soonest-expiring available credit from the freshest snapshot and generates the `redeem_request_id`. Avoids stale-UI and clock-skew races. - **Never mutates account status.** Account status is owned by usage refresh (see `usage-refresh-policy`). Reset-credit polling failure logs and retains the prior snapshot; it does not deactivate, rate-limit, or quota-block any account. -- **Dedicated scheduler, not folded into usage refresh.** Reuses the exact - `UsageRefreshScheduler` shape (leader-gated, `asyncio.Lock`-guarded, configurable - cadence) but keeps the two upstream calls decoupled. The scheduler always starts with - the app; only the interval is configurable. See `design.md` for the rationale. +- **Dedicated scheduler, not folded into usage refresh.** Reuses the `UsageRefreshScheduler` + loop shape (`asyncio.Lock`-guarded, configurable cadence) but intentionally does not use + leader election because the cache is process-local. The scheduler always starts with the + app; only the interval is configurable. See `design.md` for the rationale. ## Failure Modes @@ -52,9 +53,13 @@ client treats non-200/non-JSON responses defensively. - **Upstream 401/403/auth-expired.** Logged; prior snapshot retained. Does NOT deactivate the account. If the token is genuinely expired, usage refresh / OAuth refresh owns the deactivation path. -- **Concurrent consume clicks.** Server re-selects from the snapshot each call; the second - click either consumes a different credit (if multiple were available) or surfaces - upstream's "no available credit" error. +- **Concurrent consume clicks.** Redemption is serialized per account so two overlapping + consume requests cannot forward the same cached `credit_id` upstream. After the first + request finishes, the second request re-reads the account snapshot and either sees a + refreshed state or fails with a dashboard conflict when no credit is still available. +- **Upstream consume failures.** Client-facing upstream failures are preserved as dashboard + errors (`401`, `403`, `409`), while other consume failures surface as dashboard `503` + responses instead of falling into the generic internal-error handler. ## Example: list response @@ -96,8 +101,8 @@ client treats non-200/non-JSON responses defensively. ## Operational Notes -- The 60s cadence matches usage refresh; both are leader-gated, so adding accounts scales - upstream load by the same factor usage refresh already does. +- The 60s cadence matches usage refresh, but each replica polls because each replica serves + dashboard reads from its own process-local snapshot cache. - A credit is consumed as soon as upstream returns 200 — treat the confirmation dialog as the point of no return. diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md index db4ea734e..089182c41 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md @@ -2,15 +2,16 @@ ### Requirement: Reset credits are polled per account on a fixed cadence -The system SHALL poll upstream `GET /wham/rate-limit-reset-credits` for each eligible account on a configurable cadence that defaults to 60 seconds, using that account's stored OAuth bearer token and `chatgpt-account-id`. The scheduler SHALL always start with the application lifespan. The poll SHALL be leader-gated so that only one replica performs the polling in a multi-replica deployment. The poll SHALL skip any account that is paused, deactivated, or lacks a usable `chatgpt-account-id`. +The system SHALL poll upstream `GET /wham/rate-limit-reset-credits` for each eligible account on a configurable cadence that defaults to 60 seconds, using that account's stored OAuth bearer token and `chatgpt-account-id`. The scheduler SHALL always start with the application lifespan. Because snapshots are kept in process-local memory, every running replica SHALL refresh its own snapshot cache instead of relying on leader election. The poll SHALL skip any account that is paused, deactivated, or lacks a usable `chatgpt-account-id`. #### Scenario: Default cadence polls every 60 seconds - **WHEN** the application starts with default settings - **THEN** each eligible account's credits are fetched from upstream at most once per 60 seconds -#### Scenario: Non-leader replica does not poll -- **WHEN** leader election reports the current replica is not the leader -- **THEN** the scheduler performs no upstream reset-credits fetches in that cycle +#### Scenario: Every replica refreshes its local cache +- **WHEN** the application is deployed with multiple running replicas +- **THEN** each replica refreshes its own in-memory reset-credit snapshots on the configured cadence +- **AND** dashboard reads served by any replica can observe populated reset-credit data after that replica's refresh tick #### Scenario: Paused and deactivated accounts are skipped - **WHEN** an account is persisted as `paused` or `deactivated` @@ -36,9 +37,15 @@ The system SHALL store the most recent successful reset-credits response per acc - **THEN** subsequent reads for that account return no cached snapshot - **AND** the next scheduler tick fetches a fresh snapshot from upstream +#### Scenario: In-flight refresh cannot restore an invalidated snapshot +- **GIVEN** a scheduler refresh starts fetching reset credits for an account +- **AND** another caller invokes `invalidate(account_id)` before that refresh stores its fetched response +- **WHEN** the refresh completes +- **THEN** the stale fetched response MUST NOT be written back into the cache + ### Requirement: Operators can redeem the soonest-expiring available credit -The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` that redeems exactly one credit for the named account. The endpoint SHALL select, from the freshest cached snapshot, the credit whose `status` is `available` with the smallest `expires_at`, generate a `redeem_request_id` (UUID v4), and forward `{credit_id, redeem_request_id}` to upstream `POST /wham/rate-limit-reset-credits/consume` using the account's bearer token and `chatgpt-account-id`. On a 200 response the endpoint SHALL invalidate the cached snapshot for that account and return `{code, windows_reset, redeemed_at}`. The endpoint SHALL require dashboard write access; read-only guests MUST be refused. +The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` that redeems exactly one credit for the named account. The endpoint SHALL select, from the freshest cached snapshot, the credit whose `status` is `available` with the smallest `expires_at`, generate a `redeem_request_id` (UUID v4), and forward `{credit_id, redeem_request_id}` to upstream `POST /wham/rate-limit-reset-credits/consume` using the account's bearer token and `chatgpt-account-id`. A cached snapshot with `available_count <= 0` MUST be treated as having no redeemable credits, even if the cached `credits` list contains an item marked `available`. On a 200 response the endpoint SHALL invalidate the cached snapshot for that account and return `{code, windows_reset, redeemed_at}`. The endpoint SHALL require dashboard write access; read-only guests MUST be refused. #### Scenario: Consume selects the soonest-expiring credit - **GIVEN** an account has cached credits with expiries `2026-07-10Z` and `2026-06-20Z`, both `status: available` @@ -51,6 +58,18 @@ The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/ra - **THEN** the cached snapshot for that account is invalidated - **AND** the response returned to the dashboard is `{code, windows_reset, redeemed_at}` derived from the upstream response +#### Scenario: Concurrent consume requests for one account are serialized +- **GIVEN** two operators invoke `POST /api/accounts/{id}/rate-limit-reset-credits/consume` at nearly the same time for the same account +- **WHEN** the first request is still redeeming a credit +- **THEN** the second request MUST wait for the first request to finish before re-reading that account's cached snapshot +- **AND** the same cached `credit_id` MUST NOT be sent to upstream twice by those concurrent requests + +#### Scenario: Upstream consume failures surface as dashboard errors +- **GIVEN** an operator invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` +- **WHEN** upstream returns `401`, `403`, or `409` +- **THEN** the dashboard endpoint returns the same client-facing status class instead of a generic `500` +- **AND** other upstream consume failures return a dashboard `503` + #### Scenario: Read-only guests cannot redeem - **GIVEN** a dashboard session authenticated as a read-only guest - **WHEN** the guest invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` @@ -63,7 +82,7 @@ The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/ra ### Requirement: Reset credit polling failure does not mutate account status -The reset-credits refresh scheduler SHALL NOT transition any account's persisted status (`active`, `rate_limited`, `quota_exceeded`, `paused`, `deactivated`) in response to upstream reset-credits responses. On upstream error (non-200, non-JSON, network, or auth-like failure) the scheduler SHALL log the failure and either keep the prior cached snapshot or leave the cache unset; it SHALL NOT propagate the failure to account-status derivation. +The reset-credits refresh scheduler SHALL NOT transition any account's persisted status (`active`, `rate_limited`, `quota_exceeded`, `paused`, `deactivated`) in response to upstream reset-credits responses. On upstream error (non-200, non-JSON, malformed 200 payload, network, or auth-like failure) the scheduler SHALL log the failure and either keep the prior cached snapshot or leave the cache unset; it SHALL NOT propagate the failure to account-status derivation. #### Scenario: Upstream 401 on reset-credits does not deactivate the account - **WHEN** the scheduler receives an HTTP `401` from `GET /wham/rate-limit-reset-credits` for an account @@ -76,6 +95,12 @@ The reset-credits refresh scheduler SHALL NOT transition any account's persisted - **THEN** the cached snapshot is retained - **AND** the failure is logged +#### Scenario: Malformed 200 response is not cached as success +- **GIVEN** an account has a cached snapshot from a prior successful tick +- **WHEN** upstream returns HTTP `200` with a non-object body or a body missing required reset-credit fields +- **THEN** the response is treated as an upstream failure +- **AND** the cached snapshot is retained + ### Requirement: Reset credit polling interval is configurable The system SHALL expose setting `rate_limit_reset_credits_refresh_interval_seconds` (default `60`) to control the polling cadence. The system SHALL NOT expose a separate enable/disable toggle for reset-credit polling. diff --git a/openspec/changes/add-rate-limit-reset-credits/tasks.md b/openspec/changes/add-rate-limit-reset-credits/tasks.md index 6f358bdca..a31736d7f 100644 --- a/openspec/changes/add-rate-limit-reset-credits/tasks.md +++ b/openspec/changes/add-rate-limit-reset-credits/tasks.md @@ -7,7 +7,7 @@ ## 2. Backend scheduler, API, mapper, lifespan wiring -- [x] 2.1 Create `app/core/usage/reset_credits_refresh_scheduler.py` mirroring `app/core/usage/refresh_scheduler.py`: `RateLimitResetCreditsRefreshScheduler` dataclass with leader-gated, `asyncio.Lock`-guarded `_refresh_once` that lists accounts, skips paused/deactivated/missing-`chatgpt-account-id`, decrypts `access_token_encrypted`, calls `fetch_reset_credits`, and stores the snapshot. On upstream error: log + retain prior snapshot; do NOT mutate account status. Add `build_rate_limit_reset_credits_scheduler()` factory +- [x] 2.1 Create `app/core/usage/reset_credits_refresh_scheduler.py` mirroring `app/core/usage/refresh_scheduler.py`: `RateLimitResetCreditsRefreshScheduler` dataclass with `asyncio.Lock`-guarded `_refresh_once` that runs in every replica, lists accounts, skips paused/deactivated/missing-`chatgpt-account-id`, decrypts `access_token_encrypted`, calls `fetch_reset_credits`, and stores the snapshot. On upstream error: log + retain prior snapshot; do NOT mutate account status. Add `build_rate_limit_reset_credits_scheduler()` factory - [x] 2.2 Wire the new scheduler into `app/main.py` lifespan alongside `usage_scheduler`: build (~line 148), start (~154), stop (~314) - [x] 2.3 Create `app/modules/rate_limit_reset_credits/api.py` with `GET /api/accounts/{account_id}/rate-limit-reset-credits` (returns cached snapshot or `null`) and `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` (selects soonest-`expires_at` available credit from the freshest snapshot, generates `redeem_request_id`, calls upstream, invalidates the cached snapshot, returns `{code, windows_reset, redeemed_at}`). Use `validate_dashboard_session` for GET and `require_dashboard_write_access` for POST. Return `409` when no credit is available. Register the router in `app/main.py` - [x] 2.4 Extend the AccountSummary mapper(s) in `app/modules/accounts/` and the dashboard mapper to join the cached snapshot onto each returned account: add `available_reset_credits: int` (0 when no snapshot) and `reset_credit_nearest_expires_at: datetime | None` (null when no snapshot) @@ -26,14 +26,14 @@ - [x] 4.3 Add a reset action to `frontend/src/features/dashboard/components/account-list.tsx` (table view) inside the existing Details action cell, matching the `h-7 w-7` icon-button style with the countdown and count exposed in the `title` tooltip. Render only when `availableResetCredits > 0` - [x] 4.4 Add a `Reset (N)` button to `frontend/src/features/dashboard/components/account-card.tsx` (grid view) next to the Details button, matching the `h-7 gap-1.5` text style with the single-unit countdown label. Render only when `availableResetCredits > 0` - [x] 4.5 Implement the confirmation dialog (reuse `frontend/src/components/confirm-dialog.tsx` + `frontend/src/hooks/use-dialog-state.ts`, same shape as the delete-account dialog): body shows the soonest credit's title and `expires_at` formatted as local `YYYY-MM-DD HH:MM:SS`, plus the "credit is consumed even if the window doesn't move" warning. On confirm → call `consumeRateLimitResetCredit(accountId)` → success/failure toast → query invalidation -- [x] 4.6 Add a "Most reset credits" option to the Accounts page sort selector in `frontend/src/features/accounts/sorting.ts`: comparator orders by `availableResetCredits` desc, tiebreak by `resetCreditNearestExpiresAt` asc (soonest first), accounts with null expiry last. Add the localized dropdown label +- [x] 4.6 Add a "Most reset credits" option to the Accounts page sort selector in `frontend/src/features/accounts/sorting.ts` and make it the default Accounts page sort mode: comparator orders by `availableResetCredits` desc, tiebreak by `resetCreditNearestExpiresAt` asc (soonest first), accounts with null expiry last. Add the localized dropdown label - [x] 4.7 Add a summed reset-credit badge to `frontend/src/components/layout/app-header.tsx` for the Accounts nav tab, capped at `99+` ## 5. Tests - [x] 5.1 Backend — `app/core/clients/rate_limit_reset_credits.py`: header construction (account-id skip rule), base-url normalization, consume body shape, JSON parse on 200, error handling on non-200/non-JSON - [x] 5.2 Backend — `app/modules/rate_limit_reset_credits/store.py`: `set`/`get`/`invalidate` (single + all), concurrency under `anyio.Lock`, missing-account returns `None` -- [x] 5.3 Backend — `reset_credits_refresh_scheduler.py`: leader-gate skip, paused/deactivated account skip, one-account failure doesn't break the loop, upstream error retains prior snapshot, account status is never mutated +- [x] 5.3 Backend — `reset_credits_refresh_scheduler.py`: every replica refreshes its local cache, paused/deactivated account skip, one-account failure doesn't break the loop, upstream error retains prior snapshot, account status is never mutated - [x] 5.4 Backend — `rate_limit_reset_credits/api.py`: GET returns cached snapshot / `null` on miss; POST selects soonest expiry, calls upstream with fresh `redeem_request_id`, invalidates cache, returns `{code, windows_reset, redeemed_at}`; write-access gating refuses guests; `409` when no available credit - [x] 5.5 Backend — AccountSummary mapper: exposes the two new fields from a cached snapshot, returns `0`/`null` when no snapshot, does not crash when store is empty - [x] 5.6 Frontend — `formatSingleUnitRemaining`: boundaries at 7d (color flip), 1d, 1h, 1m, and `now`; sub-minute and past timestamps both yield `"now"` diff --git a/tests/unit/test_rate_limit_reset_credits_api.py b/tests/unit/test_rate_limit_reset_credits_api.py index 1d27809df..80bf2b64d 100644 --- a/tests/unit/test_rate_limit_reset_credits_api.py +++ b/tests/unit/test_rate_limit_reset_credits_api.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from datetime import datetime from types import SimpleNamespace from typing import Any, cast @@ -8,12 +9,19 @@ from app.core.auth.dependencies import require_dashboard_write_access from app.core.clients.rate_limit_reset_credits import ( + ConsumeResetCreditError, ConsumeResetCreditResponse, RateLimitResetCreditsSnapshot, ResetCreditItem, ) from app.core.crypto import TokenEncryptor -from app.core.exceptions import DashboardConflictError, DashboardNotFoundError, DashboardPermissionError +from app.core.exceptions import ( + DashboardAuthError, + DashboardConflictError, + DashboardNotFoundError, + DashboardPermissionError, + DashboardServiceUnavailableError, +) from app.db.models import Account, AccountStatus from app.modules.rate_limit_reset_credits import api as reset_credits_api from app.modules.rate_limit_reset_credits.api import ( @@ -121,6 +129,11 @@ def test_select_soonest_available_credit_returns_none_when_no_snapshot() -> None assert _select_soonest_available_credit(None) is None +def test_select_soonest_available_credit_respects_zero_available_count() -> None: + snapshot = _snapshot([_credit("cached_available")], available_count=0) + assert _select_soonest_available_credit(snapshot) is None + + def test_select_soonest_available_credit_returns_none_when_none_available() -> None: snapshot = _snapshot([_credit("c1", status="redeemed")]) assert _select_soonest_available_credit(snapshot) is None @@ -144,6 +157,21 @@ async def test_redeem_returns_409_when_no_available_credit() -> None: assert excinfo.value.code == "no_available_reset_credit" +@pytest.mark.asyncio +async def test_redeem_returns_409_when_cached_count_is_zero() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_1", _snapshot([_credit("cached_available")], available_count=0)) + + with pytest.raises(DashboardConflictError) as excinfo: + await _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=_raise_not_called, # type: ignore[arg-type] + ) + assert excinfo.value.code == "no_available_reset_credit" + + @pytest.mark.asyncio async def test_redeem_returns_409_when_snapshot_missing() -> None: store = RateLimitResetCreditsStore() @@ -194,7 +222,8 @@ async def consume_fn(access_token: str, account_id: str | None, credit_id: str) "account_id": "workspace-1", "credit_id": "soon", } - # Cache was invalidated for the account after success. + # Successful redemption invalidates the in-memory snapshot so the next + # dashboard refresh repulls upstream state instead of serving a local edit. assert store.get("acc_1") is None # Response shape matches the documented {code, windows_reset, redeemed_at}. assert isinstance(result, ConsumeResetCreditResponseSchema) @@ -204,6 +233,92 @@ async def consume_fn(access_token: str, account_id: str | None, credit_id: str) assert result.redeemed_at.year == 2026 +@pytest.mark.asyncio +async def test_redeem_serializes_requests_per_account() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_1", _snapshot([_credit("only")], available_count=1)) + + started = asyncio.Event() + release = asyncio.Event() + consume_calls: list[str] = [] + + async def consume_fn(access_token: str, account_id: str | None, credit_id: str) -> ConsumeResetCreditResponse: + consume_calls.append(credit_id) + started.set() + await release.wait() + return ConsumeResetCreditResponse.model_validate( + { + "code": "reset", + "credit": {"id": credit_id, "status": "redeemed", "redeemed_at": "2026-06-13T13:12:31Z"}, + "windows_reset": 1, + } + ) + + first = asyncio.create_task( + _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=consume_fn, + ) + ) + await started.wait() + + second = asyncio.create_task( + _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=consume_fn, + ) + ) + await asyncio.sleep(0) + + assert consume_calls == ["only"] + + release.set() + await first + + with pytest.raises(DashboardConflictError) as excinfo: + await second + assert excinfo.value.code == "no_available_reset_credit" + assert consume_calls == ["only"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("status_code", "expected_exception"), + [ + (401, DashboardAuthError), + (403, DashboardPermissionError), + (409, DashboardConflictError), + (503, DashboardServiceUnavailableError), + (0, DashboardServiceUnavailableError), + ], +) +async def test_redeem_translates_upstream_consume_failures( + status_code: int, + expected_exception: type[Exception], +) -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_1", _snapshot([_credit("only")], available_count=1)) + + async def consume_fn(access_token: str, account_id: str | None, credit_id: str) -> ConsumeResetCreditResponse: + raise ConsumeResetCreditError(status_code, f"upstream failed {status_code}", code=f"upstream_{status_code}") + + with pytest.raises(expected_exception) as excinfo: + await _redeem_soonest_reset_credit( + account=_account(), + store=store, + encryptor=StubEncryptor(), + consume_fn=consume_fn, + ) + + assert str(excinfo.value) == f"upstream failed {status_code}" + assert getattr(excinfo.value, "code", None) == f"upstream_{status_code}" + assert store.get("acc_1") is not None + + # --- POST consume: handler-level 404 when account missing --- diff --git a/tests/unit/test_rate_limit_reset_credits_client.py b/tests/unit/test_rate_limit_reset_credits_client.py index 1c6e4211d..4c9913226 100644 --- a/tests/unit/test_rate_limit_reset_credits_client.py +++ b/tests/unit/test_rate_limit_reset_credits_client.py @@ -22,12 +22,12 @@ class StubResponse: - def __init__(self, status: int, payload: dict | None, text: str) -> None: + def __init__(self, status: int, payload: object | None, text: str) -> None: self.status = status self._payload = payload self._text = text - async def json(self, content_type: str | None = None) -> dict: + async def json(self, content_type: str | None = None) -> object: if self._payload is None: raise ValueError("no json") return self._payload @@ -234,6 +234,42 @@ async def test_fetch_reset_credits_handles_non_json_body() -> None: assert excinfo.value.status_code == 502 +@pytest.mark.asyncio +async def test_fetch_reset_credits_rejects_malformed_success_body() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(200, ["not", "an", "object"], "")], state) + + with pytest.raises(ResetCreditFetchError) as excinfo: + await fetch_reset_credits( + "access-token", + None, + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 502 + + +@pytest.mark.asyncio +async def test_fetch_reset_credits_rejects_success_body_missing_contract_fields() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(200, {"credits": []}, "")], state) + + with pytest.raises(ResetCreditFetchError) as excinfo: + await fetch_reset_credits( + "access-token", + None, + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 502 + + @pytest.mark.asyncio async def test_consume_reset_credit_sends_credit_id_and_uuid_redeem_request_id() -> None: state = ClientState() @@ -332,6 +368,44 @@ async def test_consume_reset_credit_raises_on_non_200() -> None: assert excinfo.value.code == "no_credit" +@pytest.mark.asyncio +async def test_consume_reset_credit_rejects_malformed_success_body() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(200, "not json", "")], state) + + with pytest.raises(ConsumeResetCreditError) as excinfo: + await consume_reset_credit( + "access-token", + None, + "RateLimitResetCredit_test", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 502 + + +@pytest.mark.asyncio +async def test_consume_reset_credit_rejects_success_body_missing_contract_fields() -> None: + state = ClientState() + client = StubRetryClient([StubResponse(200, {"code": "reset"}, "")], state) + + with pytest.raises(ConsumeResetCreditError) as excinfo: + await consume_reset_credit( + "access-token", + None, + "RateLimitResetCredit_test", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + client=cast(Any, client), + ) + + assert excinfo.value.status_code == 502 + + def test_build_snapshot_projects_nearest_available_expiry() -> None: response = ResetCreditsResponse.model_validate( { diff --git a/tests/unit/test_rate_limit_reset_credits_scheduler.py b/tests/unit/test_rate_limit_reset_credits_scheduler.py index effffc953..a2e893613 100644 --- a/tests/unit/test_rate_limit_reset_credits_scheduler.py +++ b/tests/unit/test_rate_limit_reset_credits_scheduler.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from contextlib import asynccontextmanager from datetime import datetime from typing import Any @@ -164,6 +165,69 @@ async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsRes assert account.status == AccountStatus.ACTIVE +@pytest.mark.asyncio +async def test_refresh_does_not_resurrect_snapshot_invalidated_during_fetch() -> None: + store = RateLimitResetCreditsStore() + prior = RateLimitResetCreditsSnapshot(available_count=1) + await store.set("acc_redeemed", prior) + account = _make_account("acc_redeemed", status=AccountStatus.ACTIVE) + fetch_started = asyncio.Event() + release_fetch = asyncio.Event() + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + fetch_started.set() + await release_fetch.wait() + return _response(available_count=1) + + refresh_task = asyncio.create_task( + refresh_reset_credits_for_accounts( + accounts=[account], + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + ) + await fetch_started.wait() + + await store.invalidate("acc_redeemed") + release_fetch.set() + await refresh_task + + assert store.get("acc_redeemed") is None + + +@pytest.mark.asyncio +async def test_unrelated_account_write_does_not_drop_in_flight_refresh() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_a", RateLimitResetCreditsSnapshot(available_count=1)) + account = _make_account("acc_b", status=AccountStatus.ACTIVE) + fetch_started = asyncio.Event() + release_fetch = asyncio.Event() + + async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + fetch_started.set() + await release_fetch.wait() + return _response(available_count=4) + + refresh_task = asyncio.create_task( + refresh_reset_credits_for_accounts( + accounts=[account], + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + ) + await fetch_started.wait() + + await store.set("acc_a", RateLimitResetCreditsSnapshot(available_count=9)) + release_fetch.set() + await refresh_task + + snapshot_b = store.get("acc_b") + assert snapshot_b is not None + assert snapshot_b.available_count == 4 + + @pytest.mark.asyncio async def test_refresh_never_calls_account_status_writes() -> None: """The scheduler must not transition account status under any path. @@ -194,42 +258,10 @@ async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsRes @pytest.mark.asyncio -async def test_refresh_once_skips_when_not_leader(monkeypatch: pytest.MonkeyPatch) -> None: - """Non-leader replicas perform no upstream fetches and open no DB session.""" - - class NonLeader: - async def try_acquire(self) -> bool: - return False - - monkeypatch.setattr(scheduler_module, "_get_leader_election", lambda: NonLeader()) - - session_entered = False - - @asynccontextmanager - async def _forbidden_session(): # type: ignore[no-untyped-def] - nonlocal session_entered - session_entered = True - yield None - - monkeypatch.setattr(scheduler_module, "get_background_session", _forbidden_session) - - scheduler = RateLimitResetCreditsRefreshScheduler(interval_seconds=60) - await scheduler._refresh_once() - - assert session_entered is False - - -@pytest.mark.asyncio -async def test_refresh_once_leader_path_caches_snapshots(monkeypatch: pytest.MonkeyPatch) -> None: - """End-to-end leader-gated tick wires accounts -> store without status writes.""" - - class Leader: - async def try_acquire(self) -> bool: - return True - - monkeypatch.setattr(scheduler_module, "_get_leader_election", lambda: Leader()) +async def test_refresh_once_caches_snapshots_on_each_replica(monkeypatch: pytest.MonkeyPatch) -> None: + """Each process refreshes its own in-memory cache without leader gating.""" - account = _make_account("acc_leader") + account = _make_account("acc_replica") store = RateLimitResetCreditsStore() captured: list[Any] = [] @@ -265,8 +297,8 @@ async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsRes scheduler = RateLimitResetCreditsRefreshScheduler(interval_seconds=60) await scheduler._refresh_once() - assert ("fetch", "token-for-acc_leader", "workspace-x") in captured - leader_snapshot = store.get("acc_leader") - assert leader_snapshot is not None - assert leader_snapshot.available_count == 7 + assert ("fetch", "token-for-acc_replica", "workspace-x") in captured + snapshot = store.get("acc_replica") + assert snapshot is not None + assert snapshot.available_count == 7 assert account.status == AccountStatus.ACTIVE diff --git a/tests/unit/test_rate_limit_reset_credits_store.py b/tests/unit/test_rate_limit_reset_credits_store.py index 1aa90fd05..51504691b 100644 --- a/tests/unit/test_rate_limit_reset_credits_store.py +++ b/tests/unit/test_rate_limit_reset_credits_store.py @@ -1,8 +1,10 @@ from __future__ import annotations +from datetime import datetime + import pytest -from app.core.clients.rate_limit_reset_credits import RateLimitResetCreditsSnapshot +from app.core.clients.rate_limit_reset_credits import RateLimitResetCreditsSnapshot, ResetCreditItem from app.modules.rate_limit_reset_credits.store import ( RateLimitResetCreditsStore, get_rate_limit_reset_credits_store, @@ -15,6 +17,10 @@ def _snapshot(available_count: int = 1) -> RateLimitResetCreditsSnapshot: return RateLimitResetCreditsSnapshot(available_count=available_count) +def _credit(credit_id: str, *, expires_at: str, status: str = "available") -> ResetCreditItem: + return ResetCreditItem.model_validate({"id": credit_id, "expires_at": expires_at, "status": status}) + + @pytest.mark.asyncio async def test_set_and_get_round_trip() -> None: store = RateLimitResetCreditsStore() @@ -43,6 +49,44 @@ async def test_set_overwrites_prior_snapshot() -> None: assert snapshot.available_count == 5 +@pytest.mark.asyncio +async def test_generation_changes_are_scoped_to_account() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_a", _snapshot(1)) + await store.set("acc_b", _snapshot(2)) + generation_b = store.generation("acc_b") + + await store.set("acc_a", _snapshot(9)) + + assert await store.set_if_generation("acc_b", _snapshot(7), generation_b) + snapshot_b = store.get("acc_b") + assert snapshot_b is not None + assert snapshot_b.available_count == 7 + + +@pytest.mark.asyncio +async def test_same_account_generation_change_rejects_stale_write() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_a", _snapshot(1)) + generation = store.generation("acc_a") + + await store.invalidate("acc_a") + + assert not await store.set_if_generation("acc_a", _snapshot(7), generation) + assert store.get("acc_a") is None + + +@pytest.mark.asyncio +async def test_invalidate_all_rejects_in_flight_writes_for_any_account() -> None: + store = RateLimitResetCreditsStore() + generation = store.generation("acc_a") + + await store.invalidate() + + assert not await store.set_if_generation("acc_a", _snapshot(7), generation) + assert store.get("acc_a") is None + + @pytest.mark.asyncio async def test_invalidate_single_account_clears_only_that_key() -> None: store = RateLimitResetCreditsStore() @@ -76,6 +120,32 @@ async def test_invalidate_missing_account_is_noop() -> None: assert store.get("never_existed") is None +@pytest.mark.asyncio +async def test_mark_credit_redeemed_preserves_remaining_available_credits() -> None: + store = RateLimitResetCreditsStore() + await store.set( + "acc_a", + RateLimitResetCreditsSnapshot( + available_count=2, + nearest_expires_at=datetime.fromisoformat("2026-06-20T00:00:00+00:00"), + credits=[ + _credit("soon", expires_at="2026-06-20T00:00:00Z"), + _credit("late", expires_at="2026-07-10T00:00:00Z"), + ], + ), + ) + redeemed_at = datetime.fromisoformat("2026-06-18T12:00:00+00:00") + + await store.mark_credit_redeemed("acc_a", "soon", redeemed_at=redeemed_at) + + snapshot = store.get("acc_a") + assert snapshot is not None + assert snapshot.available_count == 1 + assert snapshot.nearest_expires_at == datetime.fromisoformat("2026-07-10T00:00:00+00:00") + assert [(credit.id, credit.status) for credit in snapshot.credits] == [("soon", "redeemed"), ("late", "available")] + assert snapshot.credits[0].redeemed_at == redeemed_at + + @pytest.mark.asyncio async def test_concurrent_setters_are_serialized_under_lock() -> None: store = RateLimitResetCreditsStore() From 4b51fc4637dd75b9178a9f2a6f0c56899f72060e Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Thu, 18 Jun 2026 15:54:18 +0800 Subject: [PATCH 04/39] ui adjustments --- frontend/src/components/layout/app-header.tsx | 4 +- .../reset-credit-confirm-dialog.test.tsx | 12 +-- .../reset-credit-confirm-dialog.tsx | 92 ++++++++++++------- .../features/apis/components/api-detail.tsx | 2 +- 4 files changed, 70 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/layout/app-header.tsx b/frontend/src/components/layout/app-header.tsx index 1c5eb4f25..7ceada881 100644 --- a/frontend/src/components/layout/app-header.tsx +++ b/frontend/src/components/layout/app-header.tsx @@ -91,7 +91,7 @@ export function AppHeader({ {item.label} {item.to === "/accounts" && accountsResetBadge ? ( - + {accountsResetBadge} ) : null} @@ -167,7 +167,7 @@ export function AppHeader({ > {item.label} {item.to === "/accounts" && accountsResetBadge ? ( - + {accountsResetBadge} ) : null} diff --git a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx index 7fe69e90b..2cb007b9b 100644 --- a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx +++ b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx @@ -83,9 +83,9 @@ describe("ResetCreditConfirmDialog", () => { ); const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); - // Snapshot loads the soonest credit title. - expect(await screen.findByText("Banked rate-limit reset")).toBeInTheDocument(); - expect(screen.getByText(/Expires \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)).toBeInTheDocument(); + // Snapshot loads the available count and soonest credit expiry. + expect(await screen.findByText("1 free rate limit reset")).toBeInTheDocument(); + expect(screen.getByText(/Reset expires on \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Redeem credit" })); @@ -127,7 +127,7 @@ describe("ResetCreditConfirmDialog", () => { ); const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries"); - expect(await screen.findByText("Banked rate-limit reset")).toBeInTheDocument(); + expect(await screen.findByText("1 free rate limit reset")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Redeem credit" })); @@ -181,8 +181,8 @@ describe("ResetCreditConfirmDialog", () => { />, ); - expect(await screen.findByText("Persistent banked reset")).toBeInTheDocument(); - expect(screen.getByText("No expiry provided.")).toBeInTheDocument(); + expect(await screen.findByText("1 free rate limit reset")).toBeInTheDocument(); + expect(screen.getByText("No upcoming expiry data available.")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Redeem credit" })); diff --git a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx index 47a6d6e61..c6a1becc4 100644 --- a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx +++ b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx @@ -1,5 +1,3 @@ -import { AlertTriangle } from "lucide-react"; - import { ConfirmDialog } from "@/components/confirm-dialog"; import { useAccountMutations, @@ -36,6 +34,38 @@ function pickSoonestAvailableCredit( }); } +function CreditExpiryLine({ + expiresAt, + label, + suffix, + colorClass, +}: { + expiresAt: string | null | undefined; + label: string; + suffix?: string; + colorClass?: string; +}) { + if (!expiresAt) { + return

{label}{suffix ? ` ${suffix}` : ""}

; + } + const countdown = formatSingleUnitRemaining(expiresAt); + return ( +

+ {label}{" "} + {formatLocalDateTimeSeconds(expiresAt)} + + ({countdown.label}) + + {suffix ? ` ${suffix}` : ""} +

+ ); +} + export function ResetCreditConfirmDialog({ open, onOpenChange, @@ -44,9 +74,10 @@ export function ResetCreditConfirmDialog({ const { resetCreditConsumeMutation } = useAccountMutations(); const snapshotQuery = useRateLimitResetCredits(accountId, open); const soonest = pickSoonestAvailableCredit(snapshotQuery.data?.credits); - const title = soonest?.title?.trim() || "Rate-limit reset credit"; - const expiresAt = soonest?.expiresAt ?? null; - const countdown = expiresAt ? formatSingleUnitRemaining(expiresAt) : null; + const otherCredits = (snapshotQuery.data?.credits ?? []).filter( + (c) => c.status === "available" && c.id !== soonest?.id, + ); + const availableCount = snapshotQuery.data?.availableCount ?? 0; const pending = resetCreditConsumeMutation.isPending; const handleConfirm = () => { @@ -84,33 +115,32 @@ export function ResetCreditConfirmDialog({ onOpenChange={handleOpenChange} onConfirm={handleConfirm} > -
-
-

{title}

- {expiresAt ? ( -

- Expires {formatLocalDateTimeSeconds(expiresAt)} - {countdown ? ( - - ({countdown.label}) - - ) : null} -

- ) : ( -

No expiry provided.

- )} -
-

-

+

+ {availableCount} free rate limit reset{availableCount !== 1 ? "s" : ""}

+ {soonest ? ( +
+ + {otherCredits.map((credit) => ( + + ))} + {!soonest.expiresAt && otherCredits.length === 0 ? ( +

No upcoming expiry data available.

+ ) : null} +
+ ) : availableCount > 0 ? ( +

No upcoming expiry data available.

+ ) : null}
); diff --git a/frontend/src/features/apis/components/api-detail.tsx b/frontend/src/features/apis/components/api-detail.tsx index 52b6f9a0c..8be0715cb 100644 --- a/frontend/src/features/apis/components/api-detail.tsx +++ b/frontend/src/features/apis/components/api-detail.tsx @@ -170,7 +170,7 @@ export function ApiDetail({ className={ hasDonutData ? "mt-4 border-t pt-4 lg:mt-0 lg:max-w-[75%] lg:flex-1 lg:border-t-0 lg:border-l lg:pl-4 lg:pt-0" - : "" + : "lg:w-full" } data-testid="api-trend-panel" > From 0d88dc87149253d394aa89bb67ef5cc1efc0185d Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Sat, 20 Jun 2026 01:09:09 +0800 Subject: [PATCH 05/39] feat(banked-reset): add api for query and redeem the credit (not in codex format) --- app/modules/proxy/api.py | 129 +++++ app/modules/proxy/schemas.py | 26 + app/modules/rate_limit_reset_credits/api.py | 4 +- .../reset-credit-confirm-dialog.test.tsx | 10 +- .../features/accounts/hooks/use-accounts.ts | 9 +- .../src/features/accounts/schemas.test.ts | 20 +- frontend/src/features/accounts/schemas.ts | 6 +- .../add-rate-limit-reset-credits/design.md | 15 +- .../specs/api-keys/spec.md | 41 ++ .../specs/rate-limit-reset-credits/context.md | 47 ++ .../specs/rate-limit-reset-credits/spec.md | 28 + .../add-rate-limit-reset-credits/tasks.md | 8 +- .../verify-report.md | 78 +++ tests/integration/test_v1_reset_credit.py | 541 ++++++++++++++++++ 14 files changed, 934 insertions(+), 28 deletions(-) create mode 100644 openspec/changes/add-rate-limit-reset-credits/specs/api-keys/spec.md create mode 100644 openspec/changes/add-rate-limit-reset-credits/verify-report.md create mode 100644 tests/integration/test_v1_reset_credit.py diff --git a/app/modules/proxy/api.py b/app/modules/proxy/api.py index 537dfdf90..741d0adb0 100644 --- a/app/modules/proxy/api.py +++ b/app/modules/proxy/api.py @@ -39,8 +39,10 @@ ) from app.core.clients.files import FileProxyError from app.core.clients.proxy import ProxyResponseError +from app.core.clients.rate_limit_reset_credits import ConsumeResetCreditError, ResetCreditItem, consume_reset_credit from app.core.config.settings import get_settings from app.core.config.settings_cache import get_settings_cache +from app.core.crypto import TokenEncryptor from app.core.errors import ( PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE, OpenAIErrorEnvelope, @@ -90,6 +92,7 @@ from app.db.models import Account, AccountStatus from app.db.session import get_background_session from app.dependencies import ProxyContext, get_proxy_context, get_proxy_websocket_context +from app.modules.accounts.repository import AccountsRepository from app.modules.api_keys.repository import ApiKeysRepository from app.modules.api_keys.service import ( TRAFFIC_CLASS_OPPORTUNISTIC, @@ -129,6 +132,9 @@ ModelMetadata, RateLimitStatusPayload, ReasoningLevelSchema, + V1ResetCreditEntry, + V1ResetCreditRedeemRequest, + V1ResetCreditRedeemResponse, V1UsageLimitResponse, V1UsageResponse, WarmupFailedAccount, @@ -142,6 +148,8 @@ RateLimitStatusPayloadData, RateLimitWindowSnapshotData, ) +from app.modules.rate_limit_reset_credits.api import get_reset_credit_redeem_lock +from app.modules.rate_limit_reset_credits.store import get_rate_limit_reset_credits_store from app.modules.usage.mappers import usage_history_to_window_row from app.modules.usage.repository import UsageRepository @@ -711,6 +719,127 @@ async def v1_usage( ) +def _is_reset_credit_selectable_account(account: Account) -> bool: + return account.status not in ( + AccountStatus.REAUTH_REQUIRED, + AccountStatus.DEACTIVATED, + AccountStatus.PAUSED, + ) + + +def _eligible_reset_credit_accounts(accounts: list[Account], api_key: ApiKeyData) -> list[Account]: + if api_key.account_assignment_scope_enabled: + assigned_ids = {account_id for account_id in api_key.assigned_account_ids if account_id} + requested_accounts = [account for account in accounts if account.id in assigned_ids] + else: + requested_accounts = accounts + return [account for account in requested_accounts if _is_reset_credit_selectable_account(account)] + + +def _project_reset_credit_accounts(accounts: list[Account], api_key: ApiKeyData) -> list[tuple[str, str]]: + eligible_accounts = sorted( + _eligible_reset_credit_accounts(accounts, api_key), + key=lambda account: (account.email, account.id), + ) + return [(account.id, account.email) for account in eligible_accounts] + + +def _select_soonest_available_reset_credit(account_id: str, email: str) -> V1ResetCreditEntry | None: + snapshot = get_rate_limit_reset_credits_store().get(account_id) + if snapshot is None or snapshot.available_count <= 0: + return None + + available_credits = [credit for credit in snapshot.credits if credit.status == "available"] + if not available_credits: + return None + + far_future = datetime.max.replace(tzinfo=timezone.utc) + soonest_credit = min( + available_credits, + key=lambda credit: (credit.expires_at or far_future, credit.id), + ) + return V1ResetCreditEntry( + account_id=account_id, + email=email, + redeem_id=soonest_credit.id, + expired_at=soonest_credit.expires_at, + ) + + +def _is_reset_credit_account_in_api_key_pool(account: Account | None, api_key: ApiKeyData) -> bool: + if account is None or not _is_reset_credit_selectable_account(account): + return False + if not api_key.account_assignment_scope_enabled: + return True + assigned_ids = {account_id for account_id in api_key.assigned_account_ids if account_id} + return account.id in assigned_ids + + +def _select_available_reset_credit_by_id(account_id: str, redeem_id: str) -> ResetCreditItem | None: + snapshot = get_rate_limit_reset_credits_store().get(account_id) + if snapshot is None or snapshot.available_count <= 0: + return None + for credit in snapshot.credits: + if credit.id == redeem_id and credit.status == "available": + return credit + return None + + +def _translate_v1_reset_credit_consume_error(exc: ConsumeResetCreditError) -> HTTPException: + status_code = exc.status_code if exc.status_code > 0 else 503 + return HTTPException(status_code=status_code, detail=exc.message) + + +@v1_router.get("/reset-credit", response_model=list[V1ResetCreditEntry]) +async def v1_reset_credit( + api_key: ApiKeyData = Security(validate_usage_api_key), +) -> list[V1ResetCreditEntry]: + async with get_background_session() as session: + accounts = await AccountsRepository(session).list_accounts(refresh_existing=True) + eligible_accounts = _project_reset_credit_accounts(accounts, api_key) + + response: list[V1ResetCreditEntry] = [] + for account_id, account_email in eligible_accounts: + credit = _select_soonest_available_reset_credit(account_id, account_email) + if credit is not None: + response.append(credit) + return response + + +@v1_router.post("/reset-credit", response_model=V1ResetCreditRedeemResponse) +async def v1_redeem_reset_credit( + payload: V1ResetCreditRedeemRequest, + api_key: ApiKeyData = Security(validate_usage_api_key), +) -> V1ResetCreditRedeemResponse: + async with get_background_session() as session: + account = await AccountsRepository(session).get_by_id(payload.account_id) + if not _is_reset_credit_account_in_api_key_pool(account, api_key): + raise HTTPException(status_code=403, detail="Account is outside the API key pool") + if account is None: + raise HTTPException(status_code=403, detail="Account is outside the API key pool") + account_id = account.id + access_token_encrypted = account.access_token_encrypted + chatgpt_account_id = account.chatgpt_account_id + + lock = await get_reset_credit_redeem_lock(account_id) + async with lock: + credit = _select_available_reset_credit_by_id(account_id, payload.redeem_id) + if credit is None: + raise HTTPException(status_code=409, detail="Requested reset credit is unavailable") + access_token = TokenEncryptor().decrypt(access_token_encrypted) + try: + result = await consume_reset_credit(access_token, chatgpt_account_id, credit.id) + except ConsumeResetCreditError as exc: + raise _translate_v1_reset_credit_consume_error(exc) from exc + await get_rate_limit_reset_credits_store().invalidate(account_id) + redeemed_at = result.credit.redeemed_at if result.credit else None + return V1ResetCreditRedeemResponse( + code=result.code, + windows_reset=result.windows_reset, + redeemed_at=redeemed_at, + ) + + async def _run_v1_warmup( request: Request, context: ProxyContext = Depends(get_proxy_context), diff --git a/app/modules/proxy/schemas.py b/app/modules/proxy/schemas.py index 86c694591..4a9a59baa 100644 --- a/app/modules/proxy/schemas.py +++ b/app/modules/proxy/schemas.py @@ -1,5 +1,7 @@ from __future__ import annotations +from datetime import datetime + from pydantic import BaseModel, ConfigDict, Field from app.core.clients.files import OPENAI_FILE_UPLOAD_LIMIT_BYTES, OPENAI_FILE_USE_CASE @@ -232,6 +234,30 @@ class V1UsageResponse(BaseModel): upstream_limits: list[V1UsageLimitResponse] = [] +class V1ResetCreditEntry(BaseModel): + model_config = ConfigDict(extra="forbid") + + account_id: str + email: str + redeem_id: str + expired_at: datetime | None = Field(serialization_alias="expiredAt") + + +class V1ResetCreditRedeemRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + account_id: str + redeem_id: str + + +class V1ResetCreditRedeemResponse(BaseModel): + model_config = ConfigDict(extra="forbid") + + code: str + windows_reset: int + redeemed_at: datetime | None = None + + class WarmupRequest(BaseModel): model_config = ConfigDict(extra="forbid") diff --git a/app/modules/rate_limit_reset_credits/api.py b/app/modules/rate_limit_reset_credits/api.py index 92d9f7151..157b728f7 100644 --- a/app/modules/rate_limit_reset_credits/api.py +++ b/app/modules/rate_limit_reset_credits/api.py @@ -108,7 +108,7 @@ async def _redeem_soonest_reset_credit( encryptor: TokenEncryptor, consume_fn: ConsumeFn, ) -> ConsumeResetCreditResponseSchema: - lock = await _get_redeem_lock(account.id) + lock = await get_reset_credit_redeem_lock(account.id) async with lock: snapshot = store.get(account.id) credit = _select_soonest_available_credit(snapshot) @@ -128,7 +128,7 @@ async def _redeem_soonest_reset_credit( ) -async def _get_redeem_lock(account_id: str) -> asyncio.Lock: +async def get_reset_credit_redeem_lock(account_id: str) -> asyncio.Lock: lock = _redeem_locks.get(account_id) if lock is not None: return lock diff --git a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx index 2cb007b9b..deecb5655 100644 --- a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx +++ b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx @@ -93,10 +93,8 @@ describe("ResetCreditConfirmDialog", () => { await vi.waitFor(() => expect(toastSuccess).toHaveBeenCalledWith("Rate-limit window reset (1)"), ); - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["accounts", "list"] }); - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["accounts", "trends"] }); - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["dashboard", "overview"] }); - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["dashboard", "projections"] }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["accounts"] }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["dashboard"] }); expect(onOpenChange).toHaveBeenCalledWith(false); }); @@ -134,8 +132,8 @@ describe("ResetCreditConfirmDialog", () => { await vi.waitFor(() => expect(toastError).toHaveBeenCalledWith("No reset credit available"), ); - expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["accounts", "list"] }); - expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["dashboard", "overview"] }); + expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["accounts"] }); + expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["dashboard"] }); // Failure leaves the dialog open for retry. expect(onOpenChange).not.toHaveBeenCalledWith(false); }); diff --git a/frontend/src/features/accounts/hooks/use-accounts.ts b/frontend/src/features/accounts/hooks/use-accounts.ts index 616063bdf..3672be4ec 100644 --- a/frontend/src/features/accounts/hooks/use-accounts.ts +++ b/frontend/src/features/accounts/hooks/use-accounts.ts @@ -182,15 +182,12 @@ export function useAccountMutations() { const resetCreditConsumeMutation = useMutation({ mutationFn: (accountId: string) => consumeRateLimitResetCredit(accountId), onSuccess: (data) => { - const resetCount = data.windowsReset ?? 0; + const resetCount = data.windowsReset; toast.success( `Rate-limit window${resetCount === 1 ? "" : "s"} reset (${resetCount})`, ); - void queryClient.invalidateQueries({ queryKey: ["accounts", "list"] }); - void queryClient.invalidateQueries({ queryKey: ["accounts", "trends"] }); - void queryClient.invalidateQueries({ queryKey: ["accounts", "reset-credits"] }); - void queryClient.invalidateQueries({ queryKey: ["dashboard", "overview"] }); - void queryClient.invalidateQueries({ queryKey: ["dashboard", "projections"] }); + void queryClient.invalidateQueries({ queryKey: ["accounts"] }); + void queryClient.invalidateQueries({ queryKey: ["dashboard"] }); }, onError: (error: Error) => { toast.error(error.message || "Reset credit redeem failed"); diff --git a/frontend/src/features/accounts/schemas.test.ts b/frontend/src/features/accounts/schemas.test.ts index 014586d91..d93fdf1aa 100644 --- a/frontend/src/features/accounts/schemas.test.ts +++ b/frontend/src/features/accounts/schemas.test.ts @@ -214,25 +214,31 @@ describe("RateLimitResetCreditsSnapshotSchema", () => { }); describe("ConsumeRateLimitResetCreditResponseSchema", () => { - it("parses consume responses when nullable backend fields are omitted or null", () => { + it("requires a non-null success payload", () => { expect( ConsumeRateLimitResetCreditResponseSchema.parse({ + code: "rate_limit_reset", + windowsReset: 1, redeemedAt: ISO, }), ).toMatchObject({ + code: "rate_limit_reset", + windowsReset: 1, redeemedAt: ISO, }); - expect( + expect(() => + ConsumeRateLimitResetCreditResponseSchema.parse({ + redeemedAt: ISO, + }), + ).toThrow(); + + expect(() => ConsumeRateLimitResetCreditResponseSchema.parse({ code: null, windowsReset: null, redeemedAt: null, }), - ).toMatchObject({ - code: null, - windowsReset: null, - redeemedAt: null, - }); + ).toThrow(); }); }); diff --git a/frontend/src/features/accounts/schemas.ts b/frontend/src/features/accounts/schemas.ts index 378b9cca3..7c213c221 100644 --- a/frontend/src/features/accounts/schemas.ts +++ b/frontend/src/features/accounts/schemas.ts @@ -114,9 +114,9 @@ export const RateLimitResetCreditsSnapshotSchema = z.object({ }); export const ConsumeRateLimitResetCreditResponseSchema = z.object({ - code: z.string().nullable().optional(), - windowsReset: z.number().nullable().optional(), - redeemedAt: z.iso.datetime({ offset: true }).nullable(), + code: z.string(), + windowsReset: z.number(), + redeemedAt: z.iso.datetime({ offset: true }), }); export const AccountTrendsResponseSchema = z.object({ diff --git a/openspec/changes/add-rate-limit-reset-credits/design.md b/openspec/changes/add-rate-limit-reset-credits/design.md index 24842bf98..5840d2d9a 100644 --- a/openspec/changes/add-rate-limit-reset-credits/design.md +++ b/openspec/changes/add-rate-limit-reset-credits/design.md @@ -33,9 +33,17 @@ The reference is a single-account CLI; codex-lb needs a multi-account, dashboard **Rationale:** Usage refresh owns account-status derivation and has a dense, scenario-heavy spec (`usage-refresh-policy`) tying it to quota reconciliation, cooldowns, and warm-up. Bolting credits onto that loop would couple two upstream calls and their failure modes, and muddy the usage-refresh contract. A dedicated `RateLimitResetCreditsRefreshScheduler` reuses the `UsageRefreshScheduler` loop shape (`asyncio.Lock`-guarded `_refresh_once`, interval-only configuration) and always starts with the application. Unlike usage refresh, it deliberately runs on every replica because reset-credit snapshots are process-local and dashboard reads must be consistent regardless of which replica handles the request. **Alternatives considered:** (a) Fold into `UsageRefreshScheduler._refresh_once` — rejected for the coupling above. (b) Pure passthrough via the local `wham_router` proxy — rejected because the dashboard needs the in-memory store and per-account token decryption that the proxy router does not have, and the requirement is "refresh every 60s + store in-memory." -### Decision: Server picks the soonest-expiring credit at consume time -**Rationale:** Single source of truth. The client passes only `{account_id}` to `POST /consume`; the server reads the cached snapshot, selects the available credit with the smallest `expires_at`, generates `redeem_request_id = uuid4()`, and calls upstream. This guarantees "nearest expiry_at is selected" even if the UI is stale, and avoids a client/server clock skew race. -**Alternatives considered:** Client sends the specific `credit_id` — rejected because the cached snapshot may have changed between render and click (e.g. one expired or was redeemed elsewhere). +### Decision: Dashboard consume stays "soonest", but `/v1/reset-credit` redeems an exact credit id +**Rationale:** The dashboard flow is intentionally one-click and should keep server-side selection of the soonest available credit from the freshest snapshot. API-key automation has different needs: clients first call `GET /v1/reset-credit`, receive an explicit `redeem_id`, then submit `{account_id, redeem_id}` to `POST /v1/reset-credit`. The server still validates that the account belongs to the authenticated API key's pool and that the requested credit is currently available before forwarding the exact `credit_id` upstream. +**Alternatives considered:** (a) make `/v1/reset-credit` also pick the soonest credit server-side — rejected because the caller explicitly needs a stable `redeem_id` contract. (b) make the dashboard send a specific `credit_id` — rejected because that would add stale-UI and clock-skew sensitivity to the one-click operator flow. + +### Decision: `/v1/reset-credit` lives beside `/v1/usage` and reuses API-key account-pool semantics +**Rationale:** This is a self-service API-key feature, not a dashboard feature. The existing structural match is `GET /v1/usage`, which already requires a valid Bearer key even when global proxy auth is disabled. Reusing that surface keeps the route family coherent and reuses established `assigned_account_ids` scope semantics: scoped keys see only assigned accounts; unscoped keys see all selectable accounts. +**Alternatives considered:** (a) add `/api/reset-credit` — rejected because `/api/*` in this repo is primarily dashboard/admin surface. (b) add `/api/codex/reset-credit` — rejected because the existing API-key self-service contract already lives under `/v1/*`. + +### Decision: `GET /v1/reset-credit` returns one array item per account +**Rationale:** Email is useful data for callers, but using it as the response key collides with this repo's duplicate-email account rows. Returning one array item per account avoids key collisions while still exposing the real email inside each object. The response sorts eligible accounts by email ascending, then account id ascending, for deterministic output. Each item exposes `account_id`, `email`, `redeem_id`, and `expiredAt`, which is enough for clients to choose and redeem a credit without exposing the full upstream payload. +**Alternatives considered:** (a) key by email — rejected because duplicate-email rows can overwrite each other. (b) key by account id — rejected because the caller asked for one big array. ### Decision: Expose `available_reset_credits` + `reset_credit_nearest_expires_at` on `AccountSummary` (no DB column) **Rationale:** The Accounts-page and Dashboard list both consume `AccountSummary`; joining the cached snapshot at mapper time gets the data to every UI surface with one change and zero migration. Account rows that have no cache yet return `0` / `null` and the UI hides its reset affordances for them. @@ -55,6 +63,7 @@ The reference is a single-account CLI; codex-lb needs a multi-account, dashboard - **[In-memory cache lost on restart]** → Mitigation: acceptable per requirements; the next 60s tick repopulates. UI treats missing snapshot as `available_reset_credits: 0` (hidden affordances), not an error. - **[Credit consumed even on partial reset]** (upstream behavior: "if POST returns 200, the credit is gone") → Mitigation: confirmation dialog explicitly warns that the credit is consumed regardless of whether the rate-limit window moves. On success we invalidate the cache and let the next tick reconcile. - **[Race: credit expires between render and click]** → Mitigation: server re-selects from the freshest cached snapshot at consume time and surfaces upstream's error if the chosen credit is no longer redeemable. +- **[Race: `/v1/reset-credit` client redeems a stale `redeem_id`]** → Mitigation: the POST handler re-reads the current cached snapshot, rejects credits that are no longer `available`, and only forwards the exact `redeem_id` when it still matches an available credit on an in-pool account. - **[Many accounts = many upstream calls per tick]** → Mitigation: reuse the same skip rules (paused/deactivated/missing chatgpt-account-id) and keep the interval configurable. Each replica polls so its process-local cache is useful for dashboard reads; moving snapshots to shared storage can later reduce duplicate polling if upstream load becomes a problem. - **[Guest read-only dashboard users]** → Mitigation: `POST /consume` requires `require_dashboard_write_access`; guests can see the badge/button (count is read off `AccountSummary`) but cannot redeem. diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/api-keys/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/api-keys/spec.md new file mode 100644 index 000000000..18142c344 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/specs/api-keys/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: API keys can inspect and redeem reset credits within their account pool + +The system SHALL expose `GET /v1/reset-credit` and `POST /v1/reset-credit` for API-key-authenticated self-service reset-credit access. Both routes MUST require a valid `Authorization: Bearer sk-clb-...` header even when `api_key_auth_enabled` is false globally. Validation failures MUST use the existing OpenAI error envelope used by `/v1/*` routes. + +The target account pool SHALL be derived from the authenticated API key. If `account_assignment_scope_enabled=true`, only `assigned_account_ids` SHALL be eligible. If account scope is not enabled, all selectable accounts SHALL be eligible. + +`GET /v1/reset-credit` SHALL return only credits for the authenticated key's eligible account pool. `POST /v1/reset-credit` SHALL reject requests whose `account_id` is outside that pool. + +#### Scenario: Missing API key is rejected + +- **WHEN** a client calls `GET /v1/reset-credit` or `POST /v1/reset-credit` without a Bearer token +- **THEN** the system returns 401 in the OpenAI error format + +#### Scenario: Invalid API key is rejected + +- **WHEN** a client calls `GET /v1/reset-credit` or `POST /v1/reset-credit` with an unknown, expired, or inactive Bearer key +- **THEN** the system returns 401 in the OpenAI error format + +#### Scenario: Scoped API key sees only assigned accounts + +- **WHEN** an API key has account scope enabled with assigned accounts +- **AND** the client calls `GET /v1/reset-credit` +- **THEN** the response includes reset-credit entries only for those assigned accounts + +#### Scenario: Unscoped API key can read the full selectable pool + +- **WHEN** an API key has account scope disabled +- **AND** the client calls `GET /v1/reset-credit` +- **THEN** the response may include reset-credit entries for any selectable account that currently has an available cached credit + +#### Scenario: Out-of-pool account is rejected on redeem + +- **WHEN** a client calls `POST /v1/reset-credit` with an `account_id` outside the authenticated API key's eligible pool +- **THEN** the system returns 403 without redeeming any credit + +#### Scenario: Self-service reset-credit works while global proxy auth is disabled + +- **WHEN** `api_key_auth_enabled` is false and a client calls `GET /v1/reset-credit` or `POST /v1/reset-credit` with a valid Bearer key +- **THEN** the system still authenticates that key and applies the same account-pool rules diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md index f7f1e3465..dd476c2f1 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md @@ -35,6 +35,11 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. - **Server picks the credit, not the client.** `POST /consume` takes only the account id; the server selects the soonest-expiring available credit from the freshest snapshot and generates the `redeem_request_id`. Avoids stale-UI and clock-skew races. +- **Dashboard and self-service consume differ intentionally.** The dashboard `POST /api/accounts/{id}/rate-limit-reset-credits/consume` + takes only the account id and the server selects the soonest-expiring available credit. + The API-key self-service `POST /v1/reset-credit` instead accepts `{account_id, redeem_id}` + and forwards that exact credit only after validating account-pool membership and current + credit availability. - **Never mutates account status.** Account status is owned by usage refresh (see `usage-refresh-policy`). Reset-credit polling failure logs and retains the prior snapshot; it does not deactivate, rate-limit, or quota-block any account. @@ -42,6 +47,10 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. loop shape (`asyncio.Lock`-guarded, configurable cadence) but intentionally does not use leader election because the cache is process-local. The scheduler always starts with the app; only the interval is configurable. See `design.md` for the rationale. +- **`/v1/reset-credit` follows the `/v1/usage` route family.** It is an API-key self-service + contract, not a dashboard/admin route. The authenticated key's account pool comes from + existing assignment-scope behavior: scoped keys are limited to `assigned_account_ids`, + while unscoped keys can see all selectable accounts. ## Failure Modes @@ -57,6 +66,10 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. consume requests cannot forward the same cached `credit_id` upstream. After the first request finishes, the second request re-reads the account snapshot and either sees a refreshed state or fails with a dashboard conflict when no credit is still available. +- **Stale `/v1/reset-credit` `redeem_id`.** A client may read a credit from `GET /v1/reset-credit` + and redeem it later after the snapshot changed. `POST /v1/reset-credit` re-reads the + current cached snapshot and rejects unavailable or mismatched `redeem_id` values with a + client error instead of redeeming a different credit. - **Upstream consume failures.** Client-facing upstream failures are preserved as dashboard errors (`401`, `403`, `409`), while other consume failures surface as dashboard `503` responses instead of falling into the generic internal-error handler. @@ -99,12 +112,46 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. } ``` +## Example: `/v1/reset-credit` GET response + +```json +[ + { + "account_id": "acc_alpha", + "email": "alpha@example.com", + "redeem_id": "RateLimitResetCredit_alpha", + "expiredAt": "2026-07-12T01:29:41.346025Z" + }, + { + "account_id": "acc_beta", + "email": "beta@example.com", + "redeem_id": "RateLimitResetCredit_beta", + "expiredAt": null + } +] +``` + +Accounts are ordered by email ascending and then account id ascending so the response remains +deterministic for the same eligible pool. + +## Example: `/v1/reset-credit` POST body + +```json +{ + "account_id": "acc_alpha", + "redeem_id": "RateLimitResetCredit_alpha" +} +``` + ## Operational Notes - The 60s cadence matches usage refresh, but each replica polls because each replica serves dashboard reads from its own process-local snapshot cache. - A credit is consumed as soon as upstream returns 200 — treat the confirmation dialog as the point of no return. +- `/v1/reset-credit` uses the same process-local snapshot cache as the dashboard flow, so a + client may need to retry after the next refresh tick if an account has just restarted or + recently redeemed a credit. ## Related Work diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md index 089182c41..f9494d0f4 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md @@ -80,6 +80,34 @@ The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/ra - **WHEN** the operator invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` - **THEN** the endpoint returns a `409` (or equivalent client-error) without calling upstream +### Requirement: API-key self-service reset-credit reads and exact redemption reuse the cached snapshots + +The system SHALL expose `GET /v1/reset-credit` and `POST /v1/reset-credit` as API-key-authenticated self-service routes backed by the same cached reset-credit snapshots used by the dashboard flow. `GET /v1/reset-credit` SHALL project the authenticated API key's eligible account pool into one array item per eligible account, ordered by account email ascending and then account id ascending. Each item SHALL represent exactly one account that currently has at least one available credit and SHALL include `account_id`, `email`, `redeem_id`, and `expiredAt`, where `redeem_id` and `expiredAt` come from that account's soonest-`expires_at` available credit. Accounts with no cached snapshot or no available credit SHALL be omitted from the `GET` response. + +`POST /v1/reset-credit` SHALL accept JSON body `{account_id, redeem_id}`. The endpoint SHALL reject requests whose `account_id` is outside the authenticated API key's account pool. For in-pool accounts, the endpoint SHALL re-read that account's freshest cached snapshot, verify that `redeem_id` still identifies an `available` credit on the account, forward that exact `credit_id` upstream with a generated `redeem_request_id`, and invalidate the cached snapshot for the account on a 200 response. A cached snapshot with `available_count <= 0` MUST be treated as having no redeemable credits even if the cached `credits` list contains an item marked `available`. + +#### Scenario: GET returns one soonest credit per eligible account +- **GIVEN** two in-pool accounts each have multiple cached available credits +- **WHEN** the client invokes `GET /v1/reset-credit` +- **THEN** the response contains one array item per account +- **AND** each entry's `redeem_id` is the account's soonest-expiring available credit id + +#### Scenario: GET omits accounts without available cached credits +- **GIVEN** one in-pool account has `available_count: 0` and another has no cached snapshot +- **WHEN** the client invokes `GET /v1/reset-credit` +- **THEN** neither account appears in the response array + +#### Scenario: POST rejects a redeem id that is not currently available +- **GIVEN** an in-pool account whose cached snapshot does not contain the supplied `redeem_id` as an `available` credit +- **WHEN** the client invokes `POST /v1/reset-credit` +- **THEN** the endpoint returns `409` without calling upstream + +#### Scenario: POST forwards the exact requested redeem id +- **GIVEN** an in-pool account whose cached snapshot contains `redeem_id = "credit_exact"` as an available credit +- **WHEN** the client invokes `POST /v1/reset-credit` with `{account_id, redeem_id: "credit_exact"}` +- **THEN** the upstream consume request carries `credit_id = "credit_exact"` +- **AND** a successful response invalidates the cached snapshot for that account + ### Requirement: Reset credit polling failure does not mutate account status The reset-credits refresh scheduler SHALL NOT transition any account's persisted status (`active`, `rate_limited`, `quota_exceeded`, `paused`, `deactivated`) in response to upstream reset-credits responses. On upstream error (non-200, non-JSON, malformed 200 payload, network, or auth-like failure) the scheduler SHALL log the failure and either keep the prior cached snapshot or leave the cache unset; it SHALL NOT propagate the failure to account-status derivation. diff --git a/openspec/changes/add-rate-limit-reset-credits/tasks.md b/openspec/changes/add-rate-limit-reset-credits/tasks.md index a31736d7f..98614f981 100644 --- a/openspec/changes/add-rate-limit-reset-credits/tasks.md +++ b/openspec/changes/add-rate-limit-reset-credits/tasks.md @@ -12,6 +12,10 @@ - [x] 2.3 Create `app/modules/rate_limit_reset_credits/api.py` with `GET /api/accounts/{account_id}/rate-limit-reset-credits` (returns cached snapshot or `null`) and `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` (selects soonest-`expires_at` available credit from the freshest snapshot, generates `redeem_request_id`, calls upstream, invalidates the cached snapshot, returns `{code, windows_reset, redeemed_at}`). Use `validate_dashboard_session` for GET and `require_dashboard_write_access` for POST. Return `409` when no credit is available. Register the router in `app/main.py` - [x] 2.4 Extend the AccountSummary mapper(s) in `app/modules/accounts/` and the dashboard mapper to join the cached snapshot onto each returned account: add `available_reset_credits: int` (0 when no snapshot) and `reset_credit_nearest_expires_at: datetime | None` (null when no snapshot) - [x] 2.5 Update the backend pydantic response schemas (`AccountSummary` / equivalent) to declare the two new fields +- [x] 2.6 Extend the authenticated `/v1/*` proxy surface with `GET /v1/reset-credit` and `POST /v1/reset-credit` in `app/modules/proxy/api.py`, using `validate_usage_api_key` and the existing OpenAI-style error envelope +- [x] 2.7 Reuse API-key assignment-scope semantics to resolve the eligible reset-credit account pool: scoped keys may access only `assigned_account_ids`; unscoped keys may access all selectable accounts +- [x] 2.8 Implement `GET /v1/reset-credit` projection from cached snapshots into a deterministic array ordered by account email ascending and account id ascending, returning one soonest available credit per eligible account with `email` included in each object and omitting accounts with no available cached credits +- [x] 2.9 Implement `POST /v1/reset-credit` exact-credit redemption: validate `{account_id, redeem_id}`, reject out-of-pool accounts, reject unavailable or mismatched redeem ids, forward the exact `credit_id` upstream, and invalidate the cached snapshot on success ## 3. Frontend schemas, API client, formatter @@ -42,11 +46,13 @@ - [x] 5.9 Frontend — confirm dialog → consume: confirmation calls `consumeRateLimitResetCredit`, shows the expiry in local `YYYY-MM-DD HH:MM:SS`, success path invalidates queries, failure path surfaces a toast and does not invalidate - [x] 5.10 Frontend — "Most reset credits" sort: comparator orders by count desc with soonest-expiry tiebreak, null-expiry accounts last - [x] 5.11 Frontend — Accounts nav badge: shows the summed total, caps at `99+`, and hides at zero +- [x] 5.12 Backend — `/v1/reset-credit` GET: requires Bearer API key, honors scoped vs unscoped account pools, emits a deterministic array with `email` in each object, and omits accounts without available cached credits +- [x] 5.13 Backend — `/v1/reset-credit` POST: rejects out-of-pool `account_id`, rejects unavailable or mismatched `redeem_id`, forwards the exact requested `credit_id`, invalidates the snapshot on success, and preserves `/v1/*` OpenAI-style error responses ## 6. Validation and OpenSpec hygiene - [x] 6.1 Run `openspec validate add-rate-limit-reset-credits --strict` and resolve any findings - [x] 6.2 Run `openspec validate --specs --strict` to confirm no main-spec drift -- [ ] 6.3 Run backend checks: `uv run ruff check && uv run ruff format --check && uv run pytest` (or the repo's documented equivalent) +- [x] 6.3 Run backend checks: `uv run ruff check && uv run ruff format --check && uv run pytest` (or the repo's documented equivalent) - [x] 6.4 Run frontend checks: `pnpm -C frontend lint && pnpm -C frontend typecheck && pnpm -C frontend test` (or the repo's documented equivalent) - [ ] 6.5 Manually verify the three Reset button placements, the per-button count labels, the Accounts-nav total badge cap behavior, the countdown color flip at 7d, the local expiry timestamp, the confirm flow, and the new sort option against the spec scenarios diff --git a/openspec/changes/add-rate-limit-reset-credits/verify-report.md b/openspec/changes/add-rate-limit-reset-credits/verify-report.md new file mode 100644 index 000000000..b3be3cc76 --- /dev/null +++ b/openspec/changes/add-rate-limit-reset-credits/verify-report.md @@ -0,0 +1,78 @@ +## Verification Report + +### `openspec validate add-rate-limit-reset-credits --strict` + +- Result: passed +- Output: `Change 'add-rate-limit-reset-credits' is valid` + +### `uv run pytest tests/integration/test_v1_reset_credit.py tests/unit/test_rate_limit_reset_credits_api.py -v` + +- Result: passed +- Output: `28 passed in 1.52s` + +### `uv run ruff check && uv run ruff format --check && uv run pytest` + +- First attempt: tool timeout at 120000 ms while `pytest` was still running +- Rerun with a longer timeout: passed +- `ruff check`: `All checks passed!` +- `ruff format --check`: `648 files already formatted` +- `pytest`: `3703 passed, 45 skipped, 3 warnings in 223.70s (0:03:43)` + +### Frontend Reset-Credit Contract Follow-Up + +- Focused red/green after tightening the frontend consume contract: + - `bun run test src/features/accounts/schemas.test.ts src/features/accounts/components/reset-credit-confirm-dialog.test.tsx` + - Red step failed as expected before the code change on schema strictness and top-level query invalidation expectations + - Green step passed with `12` tests passing +- Full frontend quality gate rerun after the fix: + - `bun run lint` passed + - `bun run typecheck` passed + - `bun run test` passed with `104` test files and `652` tests passing in `89.29s` +- Frontend test stderr still includes existing React `act(...)`, Recharts zero-size container, and jsdom `HTMLCanvasElement.getContext()` warnings during an otherwise passing run + +### Final Fresh Verification Snapshot + +- `openspec validate add-rate-limit-reset-credits --strict` passed +- `openspec instructions apply --change "add-rate-limit-reset-credits" --json` now reports `40/41` tasks complete with only `6.5` remaining +- `bun run lint && bun run typecheck && bun run test` passed again after the frontend contract fix + - `104` test files passed + - `652` tests passed + - Existing stderr warnings remained non-fatal + +### `openspec validate --specs --strict` + +- Result: passed +- Output: `Totals: 30 passed, 0 failed (30 items)` + +### Frontend verification + +- Repo package manager declaration: `frontend/package.json` declares `"packageManager": "bun@1.3.14"` +- Practical command form used in this worktree: `bun run lint`, `bun run typecheck`, and `bun run test` with `workdir=frontend` +- Note: `bun -C frontend ...` is not supported by the installed Bun CLI in this environment and fails with `error: Invalid Argument '-C'` + +#### `bun run lint` + +- Result: passed +- Output: `$ eslint .` + +#### `bun run typecheck` + +- Result: passed +- Output: `$ tsc -b` + +#### `bun run test` + +- Result: passed +- Output: `Test Files 104 passed (104)` +- Output: `Tests 652 passed (652)` +- Output: `Duration 91.73s` +- Notes: stderr included existing React `act(...)` warnings, Recharts zero-size container warnings, and jsdom `HTMLCanvasElement.getContext()` not-implemented warnings during the passing run + +### Manual Verification + +- Not performed in this implementation pass +- `openspec/changes/add-rate-limit-reset-credits/tasks.md` item `6.5` remains unchecked + +### Remaining OpenSpec Gaps + +- `6.5` remains unchecked because the requested manual UI verification was not performed in this implementation pass. diff --git a/tests/integration/test_v1_reset_credit.py b/tests/integration/test_v1_reset_credit.py new file mode 100644 index 000000000..eecd4ae20 --- /dev/null +++ b/tests/integration/test_v1_reset_credit.py @@ -0,0 +1,541 @@ +from __future__ import annotations + +import base64 +import json +from datetime import datetime, timedelta, timezone +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from app.core.auth import generate_unique_account_id +from app.core.clients.rate_limit_reset_credits import ( + ConsumeResetCreditResponse, + RateLimitResetCreditsSnapshot, + ResetCreditItem, +) +from app.core.config.settings import get_settings +from app.core.crypto import TokenEncryptor +from app.db.models import AccountStatus +from app.modules.rate_limit_reset_credits.store import get_rate_limit_reset_credits_store + +pytestmark = pytest.mark.integration + + +@pytest.fixture(autouse=True) +def _reset_settings_cache(): + get_settings.cache_clear() + yield + get_settings.cache_clear() + + +@pytest.fixture(autouse=True) +async def _clear_reset_credit_store(): + await get_rate_limit_reset_credits_store().invalidate() + yield + await get_rate_limit_reset_credits_store().invalidate() + + +def _encode_jwt(payload: dict[str, object]) -> str: + raw = json.dumps(payload, separators=(",", ":")).encode("utf-8") + body = base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + return f"header.{body}.sig" + + +def _make_auth_json(account_id: str, email: str) -> dict[str, object]: + payload = { + "email": email, + "chatgpt_account_id": account_id, + "https://api.openai.com/auth": {"chatgpt_plan_type": "plus"}, + } + return { + "tokens": { + "idToken": _encode_jwt(payload), + "accessToken": "access-token", + "refreshToken": "refresh-token", + "accountId": account_id, + }, + } + + +async def _import_account(async_client, account_id: str, email: str) -> str: + auth_json = _make_auth_json(account_id, email) + files = {"auth_json": ("auth.json", json.dumps(auth_json), "application/json")} + response = await async_client.post("/api/accounts/import", files=files) + assert response.status_code == 200 + return generate_unique_account_id(account_id, email) + + +async def _enable_api_key_auth(async_client) -> None: + response = await async_client.put( + "/api/settings", + json={ + "stickyThreadsEnabled": False, + "preferEarlierResetAccounts": False, + "apiKeyAuthEnabled": True, + }, + ) + assert response.status_code == 200 + + +async def _create_api_key(async_client, *, name: str) -> tuple[str, str]: + response = await async_client.post("/api/api-keys/", json={"name": name}) + assert response.status_code == 200 + payload = response.json() + return payload["id"], payload["key"] + + +async def _seed_snapshot( + account_id: str, + *, + available_count: int, + credits: list[ResetCreditItem], +) -> None: + await get_rate_limit_reset_credits_store().set( + account_id, + RateLimitResetCreditsSnapshot( + available_count=available_count, + nearest_expires_at=min( + ( + credit.expires_at + for credit in credits + if credit.status == "available" and credit.expires_at is not None + ), + default=None, + ), + credits=credits, + ), + ) + + +@pytest.mark.asyncio +async def test_v1_reset_credit_requires_valid_bearer_key(async_client): + await _enable_api_key_auth(async_client) + + missing = await async_client.get("/v1/reset-credit") + invalid = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": "Bearer invalid-key"}, + ) + + for response in (missing, invalid): + assert response.status_code == 401 + assert response.json()["error"]["code"] == "invalid_api_key" + + +@pytest.mark.asyncio +async def test_v1_reset_credit_scoped_pool_returns_assigned_account_with_soonest_available_credit(async_client): + await _enable_api_key_auth(async_client) + assigned_email = "real-assigned@example.com" + other_email = "other@example.com" + assigned_account_id = await _import_account(async_client, "acc-reset-assigned", assigned_email) + other_account_id = await _import_account(async_client, "acc-reset-other", other_email) + + key_id, key = await _create_api_key(async_client, name="reset-credit-scoped") + assign = await async_client.patch( + f"/api/api-keys/{key_id}", + json={"assignedAccountIds": [assigned_account_id]}, + ) + assert assign.status_code == 200 + + soonest = datetime(2031, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + later = soonest + timedelta(hours=2) + await _seed_snapshot( + assigned_account_id, + available_count=2, + credits=[ + ResetCreditItem(id="credit-later", status="available", expires_at=later), + ResetCreditItem(id="credit-soonest", status="available", expires_at=soonest), + ResetCreditItem(id="credit-redeemed", status="redeemed", expires_at=soonest - timedelta(hours=1)), + ], + ) + await _seed_snapshot( + other_account_id, + available_count=1, + credits=[ResetCreditItem(id="credit-other", status="available", expires_at=soonest + timedelta(days=1))], + ) + + response = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + ) + + assert response.status_code == 200 + assert response.json() == [ + { + "account_id": assigned_account_id, + "email": assigned_email, + "redeem_id": "credit-soonest", + "expiredAt": "2031-01-02T03:04:05Z", + } + ] + + +@pytest.mark.asyncio +async def test_v1_reset_credit_null_expiry_available_credit_is_returned(async_client): + await _enable_api_key_auth(async_client) + email = "null-expiry@example.com" + account_id = await _import_account(async_client, "acc-reset-null-expiry", email) + + _, key = await _create_api_key(async_client, name="reset-credit-null-expiry") + await _seed_snapshot( + account_id, + available_count=1, + credits=[ResetCreditItem(id="credit-null-expiry", status="available", expires_at=None)], + ) + + response = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + ) + + assert response.status_code == 200 + assert response.json() == [ + { + "account_id": account_id, + "email": email, + "redeem_id": "credit-null-expiry", + "expiredAt": None, + } + ] + + +@pytest.mark.asyncio +async def test_v1_reset_credit_mixed_null_expiry_prefers_dated_credit(async_client): + await _enable_api_key_auth(async_client) + email = "mixed-null-expiry@example.com" + account_id = await _import_account(async_client, "acc-reset-mixed-null-expiry", email) + + _, key = await _create_api_key(async_client, name="reset-credit-mixed-null-expiry") + expires_at = datetime(2031, 2, 1, 1, 2, 3, tzinfo=timezone.utc) + await _seed_snapshot( + account_id, + available_count=2, + credits=[ + ResetCreditItem(id="credit-null-expiry", status="available", expires_at=None), + ResetCreditItem(id="credit-dated", status="available", expires_at=expires_at), + ], + ) + + response = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + ) + + assert response.status_code == 200 + assert response.json() == [ + { + "account_id": account_id, + "email": email, + "redeem_id": "credit-dated", + "expiredAt": "2031-02-01T01:02:03Z", + } + ] + + +@pytest.mark.asyncio +async def test_v1_reset_credit_selectable_accounts_excludes_paused_accounts(async_client): + await _enable_api_key_auth(async_client) + active_email = "active@example.com" + paused_email = "paused@example.com" + active_account_id = await _import_account(async_client, "acc-reset-active", active_email) + paused_account_id = await _import_account(async_client, "acc-reset-paused", paused_email) + + pause = await async_client.post( + f"/api/accounts/{paused_account_id}/pause", + json={"reason": "test pause"}, + ) + assert pause.status_code == 200 + + _, key = await _create_api_key(async_client, name="reset-credit-unscoped") + expires_at = datetime(2031, 2, 3, 4, 5, 6, tzinfo=timezone.utc) + await _seed_snapshot( + active_account_id, + available_count=1, + credits=[ResetCreditItem(id="credit-active", status="available", expires_at=expires_at)], + ) + await _seed_snapshot( + paused_account_id, + available_count=1, + credits=[ResetCreditItem(id="credit-paused", status="available", expires_at=expires_at - timedelta(hours=1))], + ) + + response = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + ) + + assert response.status_code == 200 + assert response.json() == [ + { + "account_id": active_account_id, + "email": active_email, + "redeem_id": "credit-active", + "expiredAt": "2031-02-03T04:05:06Z", + } + ] + + +@pytest.mark.asyncio +async def test_v1_reset_credit_duplicate_email_accounts_return_separate_entries(async_client): + await _enable_api_key_auth(async_client) + shared_email = "duplicate@example.com" + first_account_id = await _import_account(async_client, "acc-reset-duplicate-1", shared_email) + second_account_id = await _import_account(async_client, "acc-reset-duplicate-2", shared_email) + + _, key = await _create_api_key(async_client, name="reset-credit-duplicate-email") + first_expires_at = datetime(2031, 3, 4, 5, 6, 7, tzinfo=timezone.utc) + second_expires_at = first_expires_at + timedelta(hours=1) + await _seed_snapshot( + first_account_id, + available_count=1, + credits=[ResetCreditItem(id="credit-duplicate-1", status="available", expires_at=first_expires_at)], + ) + await _seed_snapshot( + second_account_id, + available_count=1, + credits=[ResetCreditItem(id="credit-duplicate-2", status="available", expires_at=second_expires_at)], + ) + + response = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + ) + + assert response.status_code == 200 + assert response.json() == [ + { + "account_id": first_account_id, + "email": shared_email, + "redeem_id": "credit-duplicate-1", + "expiredAt": "2031-03-04T05:06:07Z", + }, + { + "account_id": second_account_id, + "email": shared_email, + "redeem_id": "credit-duplicate-2", + "expiredAt": "2031-03-04T06:06:07Z", + }, + ] + + +@pytest.mark.asyncio +async def test_v1_reset_credit_post_outside_api_key_scope_returns_403(async_client, monkeypatch: pytest.MonkeyPatch): + await _enable_api_key_auth(async_client) + allowed_account_id = await _import_account(async_client, "acc-reset-post-allowed", "allowed@example.com") + blocked_account_id = await _import_account(async_client, "acc-reset-post-blocked", "blocked@example.com") + + key_id, key = await _create_api_key(async_client, name="reset-credit-post-scope") + assign = await async_client.patch( + f"/api/api-keys/{key_id}", + json={"assignedAccountIds": [allowed_account_id]}, + ) + assert assign.status_code == 200 + + consume_mock = AsyncMock() + monkeypatch.setattr("app.modules.proxy.api.consume_reset_credit", consume_mock) + + response = await async_client.post( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + json={"account_id": blocked_account_id, "redeem_id": "credit-blocked"}, + ) + + assert response.status_code == 403 + assert response.json()["error"]["type"] == "permission_error" + consume_mock.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_v1_reset_credit_post_unavailable_redeem_id_returns_409(async_client, monkeypatch: pytest.MonkeyPatch): + await _enable_api_key_auth(async_client) + account_id = await _import_account(async_client, "acc-reset-post-missing", "missing@example.com") + + _, key = await _create_api_key(async_client, name="reset-credit-post-missing") + await _seed_snapshot( + account_id, + available_count=1, + credits=[ + ResetCreditItem( + id="credit-available", + status="available", + expires_at=datetime(2031, 4, 1, tzinfo=timezone.utc), + ) + ], + ) + + consume_mock = AsyncMock() + monkeypatch.setattr("app.modules.proxy.api.consume_reset_credit", consume_mock) + + response = await async_client.post( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + json={"account_id": account_id, "redeem_id": "credit-missing"}, + ) + + assert response.status_code == 409 + assert response.json()["error"]["code"] == "invalid_request_error" + consume_mock.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_v1_reset_credit_post_consumes_exact_credit_and_invalidates_snapshot( + async_client, + monkeypatch: pytest.MonkeyPatch, +): + await _enable_api_key_auth(async_client) + email = "exact-credit@example.com" + account_id = await _import_account(async_client, "acc-reset-post-exact", email) + + _, key = await _create_api_key(async_client, name="reset-credit-post-exact") + soonest = datetime(2031, 5, 1, 1, 0, 0, tzinfo=timezone.utc) + later = soonest + timedelta(hours=2) + await _seed_snapshot( + account_id, + available_count=2, + credits=[ + ResetCreditItem(id="credit-soonest", status="available", expires_at=soonest), + ResetCreditItem(id="credit-later", status="available", expires_at=later), + ], + ) + + consume_mock = AsyncMock( + return_value=ConsumeResetCreditResponse.model_validate( + { + "code": "reset", + "credit": {"id": "credit-later", "status": "redeemed", "redeemed_at": "2031-05-01T03:30:00Z"}, + "windows_reset": 1, + } + ) + ) + monkeypatch.setattr("app.modules.proxy.api.consume_reset_credit", consume_mock) + + response = await async_client.post( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + json={"account_id": account_id, "redeem_id": "credit-later"}, + ) + + assert response.status_code == 200 + assert response.json() == { + "code": "reset", + "windows_reset": 1, + "redeemed_at": "2031-05-01T03:30:00Z", + } + consume_mock.assert_awaited_once() + assert consume_mock.await_args.args[2] == "credit-later" + assert get_rate_limit_reset_credits_store().get(account_id) is None + + +@pytest.mark.asyncio +async def test_v1_reset_credit_post_closes_session_before_lock_and_upstream_consume( + async_client, + monkeypatch: pytest.MonkeyPatch, +): + await _enable_api_key_auth(async_client) + account_id = await _import_account( + async_client, + "acc-reset-post-session-lifecycle", + "session-lifecycle@example.com", + ) + + _, key = await _create_api_key(async_client, name="reset-credit-post-session-lifecycle") + await _seed_snapshot( + account_id, + available_count=1, + credits=[ + ResetCreditItem( + id="credit-session-lifecycle", + status="available", + expires_at=datetime(2031, 6, 1, tzinfo=timezone.utc), + ) + ], + ) + + events: list[str] = [] + session = object() + account = SimpleNamespace( + id=account_id, + status=AccountStatus.ACTIVE, + access_token_encrypted=TokenEncryptor().encrypt("access-token"), + chatgpt_account_id="chatgpt-session-lifecycle", + ) + + class SessionManager: + async def __aenter__(self): + events.append("session_enter") + return session + + async def __aexit__(self, exc_type, exc, tb): + events.append("session_exit") + return False + + class StubAccountsRepository: + def __init__(self, repo_session): + events.append("repo_init") + assert repo_session is session + + async def get_by_id(self, requested_account_id: str): + events.append("repo_get") + assert requested_account_id == account_id + return account + + class StubLock: + async def __aenter__(self): + events.append("lock_enter") + return self + + async def __aexit__(self, exc_type, exc, tb): + events.append("lock_exit") + return False + + async def fake_get_lock(requested_account_id: str): + events.append("lock_wait") + assert requested_account_id == account_id + return StubLock() + + async def fake_consume(access_token: str, chatgpt_account_id: str, credit_id: str): + events.append("consume") + assert access_token == "access-token" + assert chatgpt_account_id == "chatgpt-session-lifecycle" + assert credit_id == "credit-session-lifecycle" + return ConsumeResetCreditResponse.model_validate( + { + "code": "reset", + "credit": { + "id": credit_id, + "status": "redeemed", + "redeemed_at": "2031-06-01T00:30:00Z", + }, + "windows_reset": 1, + } + ) + + monkeypatch.setattr("app.modules.proxy.api.get_background_session", lambda: SessionManager()) + monkeypatch.setattr("app.modules.proxy.api.AccountsRepository", StubAccountsRepository) + monkeypatch.setattr("app.modules.proxy.api.get_reset_credit_redeem_lock", fake_get_lock) + monkeypatch.setattr("app.modules.proxy.api.consume_reset_credit", fake_consume) + + response = await async_client.post( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + json={"account_id": account_id, "redeem_id": "credit-session-lifecycle"}, + ) + + assert response.status_code == 200 + assert response.json() == { + "code": "reset", + "windows_reset": 1, + "redeemed_at": "2031-06-01T00:30:00Z", + } + assert events == [ + "session_enter", + "repo_init", + "repo_get", + "session_exit", + "lock_wait", + "lock_enter", + "consume", + "lock_exit", + ] + assert get_rate_limit_reset_credits_store().get(account_id) is None From bb93f32f2b319916d3055d2712a2e4d918fdf4ef Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Sat, 20 Jun 2026 01:22:07 +0800 Subject: [PATCH 06/39] fix api to get all redeemable credit --- app/modules/proxy/api.py | 27 ++++++++++--------- .../specs/rate-limit-reset-credits/context.md | 10 +++++-- .../specs/rate-limit-reset-credits/spec.md | 8 +++--- .../add-rate-limit-reset-credits/tasks.md | 4 +-- tests/integration/test_v1_reset_credit.py | 16 +++++++++-- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/app/modules/proxy/api.py b/app/modules/proxy/api.py index 741d0adb0..1d53b78df 100644 --- a/app/modules/proxy/api.py +++ b/app/modules/proxy/api.py @@ -744,26 +744,29 @@ def _project_reset_credit_accounts(accounts: list[Account], api_key: ApiKeyData) return [(account.id, account.email) for account in eligible_accounts] -def _select_soonest_available_reset_credit(account_id: str, email: str) -> V1ResetCreditEntry | None: +def _list_available_reset_credits(account_id: str, email: str) -> list[V1ResetCreditEntry]: snapshot = get_rate_limit_reset_credits_store().get(account_id) if snapshot is None or snapshot.available_count <= 0: - return None + return [] available_credits = [credit for credit in snapshot.credits if credit.status == "available"] if not available_credits: - return None + return [] far_future = datetime.max.replace(tzinfo=timezone.utc) - soonest_credit = min( + ordered_credits = sorted( available_credits, key=lambda credit: (credit.expires_at or far_future, credit.id), ) - return V1ResetCreditEntry( - account_id=account_id, - email=email, - redeem_id=soonest_credit.id, - expired_at=soonest_credit.expires_at, - ) + return [ + V1ResetCreditEntry( + account_id=account_id, + email=email, + redeem_id=credit.id, + expired_at=credit.expires_at, + ) + for credit in ordered_credits + ] def _is_reset_credit_account_in_api_key_pool(account: Account | None, api_key: ApiKeyData) -> bool: @@ -800,9 +803,7 @@ async def v1_reset_credit( response: list[V1ResetCreditEntry] = [] for account_id, account_email in eligible_accounts: - credit = _select_soonest_available_reset_credit(account_id, account_email) - if credit is not None: - response.append(credit) + response.extend(_list_available_reset_credits(account_id, account_email)) return response diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md index dd476c2f1..2f311682b 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md @@ -122,6 +122,12 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. "redeem_id": "RateLimitResetCredit_alpha", "expiredAt": "2026-07-12T01:29:41.346025Z" }, + { + "account_id": "acc_alpha", + "email": "alpha@example.com", + "redeem_id": "RateLimitResetCredit_alpha_2", + "expiredAt": null + }, { "account_id": "acc_beta", "email": "beta@example.com", @@ -131,8 +137,8 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. ] ``` -Accounts are ordered by email ascending and then account id ascending so the response remains -deterministic for the same eligible pool. +Entries are ordered by email ascending, then account id ascending, then credit expiry ascending +with `null` expiries last, so the response remains deterministic for the same eligible pool. ## Example: `/v1/reset-credit` POST body diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md index f9494d0f4..0632293fa 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md @@ -82,15 +82,15 @@ The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/ra ### Requirement: API-key self-service reset-credit reads and exact redemption reuse the cached snapshots -The system SHALL expose `GET /v1/reset-credit` and `POST /v1/reset-credit` as API-key-authenticated self-service routes backed by the same cached reset-credit snapshots used by the dashboard flow. `GET /v1/reset-credit` SHALL project the authenticated API key's eligible account pool into one array item per eligible account, ordered by account email ascending and then account id ascending. Each item SHALL represent exactly one account that currently has at least one available credit and SHALL include `account_id`, `email`, `redeem_id`, and `expiredAt`, where `redeem_id` and `expiredAt` come from that account's soonest-`expires_at` available credit. Accounts with no cached snapshot or no available credit SHALL be omitted from the `GET` response. +The system SHALL expose `GET /v1/reset-credit` and `POST /v1/reset-credit` as API-key-authenticated self-service routes backed by the same cached reset-credit snapshots used by the dashboard flow. `GET /v1/reset-credit` SHALL project the authenticated API key's eligible account pool into one array item per available credit, ordered by account email ascending, then account id ascending, then credit `expires_at` ascending with `null` expiries last, then credit id ascending. Each item SHALL include `account_id`, `email`, `redeem_id`, and `expiredAt`, where `redeem_id` and `expiredAt` come from that available credit. Accounts with no cached snapshot or no available credit SHALL be omitted from the `GET` response. `POST /v1/reset-credit` SHALL accept JSON body `{account_id, redeem_id}`. The endpoint SHALL reject requests whose `account_id` is outside the authenticated API key's account pool. For in-pool accounts, the endpoint SHALL re-read that account's freshest cached snapshot, verify that `redeem_id` still identifies an `available` credit on the account, forward that exact `credit_id` upstream with a generated `redeem_request_id`, and invalidate the cached snapshot for the account on a 200 response. A cached snapshot with `available_count <= 0` MUST be treated as having no redeemable credits even if the cached `credits` list contains an item marked `available`. -#### Scenario: GET returns one soonest credit per eligible account +#### Scenario: GET returns every available credit for eligible accounts - **GIVEN** two in-pool accounts each have multiple cached available credits - **WHEN** the client invokes `GET /v1/reset-credit` -- **THEN** the response contains one array item per account -- **AND** each entry's `redeem_id` is the account's soonest-expiring available credit id +- **THEN** the response contains one array item per available credit +- **AND** entries are grouped by account email and account id, with each account's credits ordered by soonest expiry first and `null` expiries last #### Scenario: GET omits accounts without available cached credits - **GIVEN** one in-pool account has `available_count: 0` and another has no cached snapshot diff --git a/openspec/changes/add-rate-limit-reset-credits/tasks.md b/openspec/changes/add-rate-limit-reset-credits/tasks.md index 98614f981..7a45451b0 100644 --- a/openspec/changes/add-rate-limit-reset-credits/tasks.md +++ b/openspec/changes/add-rate-limit-reset-credits/tasks.md @@ -14,7 +14,7 @@ - [x] 2.5 Update the backend pydantic response schemas (`AccountSummary` / equivalent) to declare the two new fields - [x] 2.6 Extend the authenticated `/v1/*` proxy surface with `GET /v1/reset-credit` and `POST /v1/reset-credit` in `app/modules/proxy/api.py`, using `validate_usage_api_key` and the existing OpenAI-style error envelope - [x] 2.7 Reuse API-key assignment-scope semantics to resolve the eligible reset-credit account pool: scoped keys may access only `assigned_account_ids`; unscoped keys may access all selectable accounts -- [x] 2.8 Implement `GET /v1/reset-credit` projection from cached snapshots into a deterministic array ordered by account email ascending and account id ascending, returning one soonest available credit per eligible account with `email` included in each object and omitting accounts with no available cached credits +- [x] 2.8 Implement `GET /v1/reset-credit` projection from cached snapshots into a deterministic array ordered by account email ascending, account id ascending, and credit expiry ascending (`null` last), returning every available credit for each eligible account with `email` included in each object and omitting accounts with no available cached credits - [x] 2.9 Implement `POST /v1/reset-credit` exact-credit redemption: validate `{account_id, redeem_id}`, reject out-of-pool accounts, reject unavailable or mismatched redeem ids, forward the exact `credit_id` upstream, and invalidate the cached snapshot on success ## 3. Frontend schemas, API client, formatter @@ -46,7 +46,7 @@ - [x] 5.9 Frontend — confirm dialog → consume: confirmation calls `consumeRateLimitResetCredit`, shows the expiry in local `YYYY-MM-DD HH:MM:SS`, success path invalidates queries, failure path surfaces a toast and does not invalidate - [x] 5.10 Frontend — "Most reset credits" sort: comparator orders by count desc with soonest-expiry tiebreak, null-expiry accounts last - [x] 5.11 Frontend — Accounts nav badge: shows the summed total, caps at `99+`, and hides at zero -- [x] 5.12 Backend — `/v1/reset-credit` GET: requires Bearer API key, honors scoped vs unscoped account pools, emits a deterministic array with `email` in each object, and omits accounts without available cached credits +- [x] 5.12 Backend — `/v1/reset-credit` GET: requires Bearer API key, honors scoped vs unscoped account pools, emits a deterministic array with `email` in each object for every available credit, and omits accounts without available cached credits - [x] 5.13 Backend — `/v1/reset-credit` POST: rejects out-of-pool `account_id`, rejects unavailable or mismatched `redeem_id`, forwards the exact requested `credit_id`, invalidates the snapshot on success, and preserves `/v1/*` OpenAI-style error responses ## 6. Validation and OpenSpec hygiene diff --git a/tests/integration/test_v1_reset_credit.py b/tests/integration/test_v1_reset_credit.py index eecd4ae20..012805229 100644 --- a/tests/integration/test_v1_reset_credit.py +++ b/tests/integration/test_v1_reset_credit.py @@ -124,7 +124,7 @@ async def test_v1_reset_credit_requires_valid_bearer_key(async_client): @pytest.mark.asyncio -async def test_v1_reset_credit_scoped_pool_returns_assigned_account_with_soonest_available_credit(async_client): +async def test_v1_reset_credit_scoped_pool_returns_all_available_credits_for_assigned_account(async_client): await _enable_api_key_auth(async_client) assigned_email = "real-assigned@example.com" other_email = "other@example.com" @@ -167,6 +167,12 @@ async def test_v1_reset_credit_scoped_pool_returns_assigned_account_with_soonest "email": assigned_email, "redeem_id": "credit-soonest", "expiredAt": "2031-01-02T03:04:05Z", + }, + { + "account_id": assigned_account_id, + "email": assigned_email, + "redeem_id": "credit-later", + "expiredAt": "2031-01-02T05:04:05Z", } ] @@ -201,7 +207,7 @@ async def test_v1_reset_credit_null_expiry_available_credit_is_returned(async_cl @pytest.mark.asyncio -async def test_v1_reset_credit_mixed_null_expiry_prefers_dated_credit(async_client): +async def test_v1_reset_credit_mixed_null_expiry_orders_dated_credit_before_null_expiry(async_client): await _enable_api_key_auth(async_client) email = "mixed-null-expiry@example.com" account_id = await _import_account(async_client, "acc-reset-mixed-null-expiry", email) @@ -229,6 +235,12 @@ async def test_v1_reset_credit_mixed_null_expiry_prefers_dated_credit(async_clie "email": email, "redeem_id": "credit-dated", "expiredAt": "2031-02-01T01:02:03Z", + }, + { + "account_id": account_id, + "email": email, + "redeem_id": "credit-null-expiry", + "expiredAt": None, } ] From 924bdef5cab2915015cc5d62cd9f6e77203cd540 Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Sat, 20 Jun 2026 01:33:18 +0800 Subject: [PATCH 07/39] ty & ruff --- tests/integration/test_v1_reset_credit.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_v1_reset_credit.py b/tests/integration/test_v1_reset_credit.py index 012805229..d0a12993b 100644 --- a/tests/integration/test_v1_reset_credit.py +++ b/tests/integration/test_v1_reset_credit.py @@ -43,7 +43,7 @@ def _encode_jwt(payload: dict[str, object]) -> str: def _make_auth_json(account_id: str, email: str) -> dict[str, object]: - payload = { + payload: dict[str, object] = { "email": email, "chatgpt_account_id": account_id, "https://api.openai.com/auth": {"chatgpt_plan_type": "plus"}, @@ -173,7 +173,7 @@ async def test_v1_reset_credit_scoped_pool_returns_all_available_credits_for_ass "email": assigned_email, "redeem_id": "credit-later", "expiredAt": "2031-01-02T05:04:05Z", - } + }, ] @@ -241,7 +241,7 @@ async def test_v1_reset_credit_mixed_null_expiry_orders_dated_credit_before_null "email": email, "redeem_id": "credit-null-expiry", "expiredAt": None, - } + }, ] @@ -435,7 +435,9 @@ async def test_v1_reset_credit_post_consumes_exact_credit_and_invalidates_snapsh "redeemed_at": "2031-05-01T03:30:00Z", } consume_mock.assert_awaited_once() - assert consume_mock.await_args.args[2] == "credit-later" + consume_args = consume_mock.await_args + assert consume_args is not None + assert consume_args.args[2] == "credit-later" assert get_rate_limit_reset_credits_store().get(account_id) is None From 8585843011d4ed0e14373ce827f32ab163a4d2f1 Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Sat, 20 Jun 2026 01:56:41 +0800 Subject: [PATCH 08/39] review --- app/core/clients/rate_limit_reset_credits.py | 204 +++++++++++++++++- .../usage/reset_credits_refresh_scheduler.py | 38 +++- app/modules/proxy/api.py | 26 ++- app/modules/rate_limit_reset_credits/api.py | 23 +- .../src/features/accounts/schemas.test.ts | 25 ++- frontend/src/features/accounts/schemas.ts | 2 +- tests/integration/test_v1_reset_credit.py | 55 ++++- .../unit/test_rate_limit_reset_credits_api.py | 43 +++- .../test_rate_limit_reset_credits_client.py | 87 ++++++++ ...test_rate_limit_reset_credits_scheduler.py | 21 +- 10 files changed, 499 insertions(+), 25 deletions(-) diff --git a/app/core/clients/rate_limit_reset_credits.py b/app/core/clients/rate_limit_reset_credits.py index 49772c0b9..9c922d6ca 100644 --- a/app/core/clients/rate_limit_reset_credits.py +++ b/app/core/clients/rate_limit_reset_credits.py @@ -9,10 +9,17 @@ from aiohttp_retry import ExponentialRetry, RetryClient from pydantic import BaseModel, ConfigDict, Field, ValidationError +from app.core.clients.codex import ( + CodexClient, + CodexTransportError, + create_codex_session, + require_route_or_direct_egress_opt_in, +) from app.core.clients.headers import build_chatgpt_auth_headers from app.core.clients.http import lease_retry_client from app.core.config.settings import get_settings from app.core.types import JsonObject +from app.core.upstream_proxy import ResolvedUpstreamRoute from app.core.utils.request_id import get_request_id RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504} @@ -99,6 +106,9 @@ async def fetch_reset_credits( timeout_seconds: float | None = None, max_retries: int | None = None, client: RetryClient | None = None, + route: ResolvedUpstreamRoute | None = None, + codex_client: CodexClient | None = None, + allow_direct_egress: bool = False, ) -> ResetCreditsResponse: settings = get_settings() usage_base = base_url or settings.upstream_base_url @@ -107,8 +117,22 @@ async def fetch_reset_credits( retries = max_retries if max_retries is not None else settings.usage_fetch_max_retries headers = build_chatgpt_auth_headers(access_token, account_id) retry_options = _retry_options(retries + 1) + require_route_or_direct_egress_opt_in( + route=route, + allow_direct_egress=allow_direct_egress, + operation="reset credits fetch", + ) try: + if route is not None: + return await _fetch_reset_credits_via_codex( + url=url, + route=route, + headers=headers, + timeout_seconds=timeout_seconds or settings.usage_fetch_timeout_seconds, + retries=retries, + codex_client=codex_client, + ) async with lease_retry_client(client) as retry_client: async with retry_client.request( "GET", @@ -134,7 +158,7 @@ async def fetch_reset_credits( except (ValueError, ValidationError) as exc: logger.warning("Reset credits fetch invalid payload request_id=%s", get_request_id()) raise ResetCreditFetchError(502, "Invalid reset credits payload") from exc - except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + except (aiohttp.ClientError, asyncio.TimeoutError, CodexTransportError) as exc: logger.warning("Reset credits fetch error request_id=%s error=%s", get_request_id(), exc) raise ResetCreditFetchError(0, f"Reset credits fetch failed: {exc}") from exc @@ -148,12 +172,15 @@ async def consume_reset_credit( timeout_seconds: float | None = None, max_retries: int | None = None, client: RetryClient | None = None, + route: ResolvedUpstreamRoute | None = None, + codex_client: CodexClient | None = None, + allow_direct_egress: bool = False, ) -> ConsumeResetCreditResponse: settings = get_settings() usage_base = base_url or settings.upstream_base_url url = _consume_url(usage_base) timeout = aiohttp.ClientTimeout(total=timeout_seconds or settings.usage_fetch_timeout_seconds) - retries = max_retries if max_retries is not None else settings.usage_fetch_max_retries + retries = max_retries if max_retries is not None else 0 headers = build_chatgpt_auth_headers( access_token, account_id, @@ -162,8 +189,23 @@ async def consume_reset_credit( redeem_request_id = str(uuid.uuid4()) body = {"credit_id": credit_id, "redeem_request_id": redeem_request_id} retry_options = _retry_options(retries + 1) + require_route_or_direct_egress_opt_in( + route=route, + allow_direct_egress=allow_direct_egress, + operation="reset credits consume", + ) try: + if route is not None: + return await _consume_reset_credit_via_codex( + url=url, + route=route, + headers=headers, + body=body, + timeout_seconds=timeout_seconds or settings.usage_fetch_timeout_seconds, + retries=retries, + codex_client=codex_client, + ) async with lease_retry_client(client) as retry_client: async with retry_client.request( "POST", @@ -190,11 +232,128 @@ async def consume_reset_credit( except (ValueError, ValidationError) as exc: logger.warning("Reset credits consume invalid payload request_id=%s", get_request_id()) raise ConsumeResetCreditError(502, "Invalid reset credits consume payload") from exc - except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + except (aiohttp.ClientError, asyncio.TimeoutError, CodexTransportError) as exc: logger.warning("Reset credits consume error request_id=%s error=%s", get_request_id(), exc) raise ConsumeResetCreditError(0, f"Reset credits consume failed: {exc}") from exc +async def _fetch_reset_credits_via_codex( + *, + url: str, + route: ResolvedUpstreamRoute, + headers: dict[str, str], + timeout_seconds: float, + retries: int, + codex_client: CodexClient | None, +) -> ResetCreditsResponse: + attempts = max(1, retries + 1) + owns_codex_client = codex_client is None + active_codex_client = codex_client or CodexClient(create_codex_session()) + try: + for attempt in range(attempts): + try: + data, status = await _request_json_via_codex( + active_codex_client, + "GET", + url, + route=route, + headers=headers, + timeout_seconds=timeout_seconds, + ) + except CodexTransportError: + if attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + raise + if status in RETRYABLE_STATUS and attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + if status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits fetch failed ({status})" + raise ResetCreditFetchError(status, message, code=code) + try: + return ResetCreditsResponse.model_validate(_success_payload(data)) + except (ValueError, ValidationError) as exc: + raise ResetCreditFetchError(502, "Invalid reset credits payload") from exc + finally: + if owns_codex_client: + close = getattr(active_codex_client, "close", None) + if callable(close): + await close() + raise RuntimeError("unreachable reset credits fetch retry state") + + +async def _consume_reset_credit_via_codex( + *, + url: str, + route: ResolvedUpstreamRoute, + headers: dict[str, str], + body: JsonObject, + timeout_seconds: float, + retries: int, + codex_client: CodexClient | None, +) -> ConsumeResetCreditResponse: + attempts = max(1, retries + 1) + owns_codex_client = codex_client is None + active_codex_client = codex_client or CodexClient(create_codex_session()) + try: + for attempt in range(attempts): + try: + data, status = await _request_json_via_codex( + active_codex_client, + "POST", + url, + route=route, + headers=headers, + json_body=body, + timeout_seconds=timeout_seconds, + ) + except CodexTransportError: + if attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + raise + if status in RETRYABLE_STATUS and attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + if status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits consume failed ({status})" + raise ConsumeResetCreditError(status, message, code=code) + try: + return ConsumeResetCreditResponse.model_validate(_success_payload(data)) + except (ValueError, ValidationError) as exc: + raise ConsumeResetCreditError(502, "Invalid reset credits consume payload") from exc + finally: + if owns_codex_client: + close = getattr(active_codex_client, "close", None) + if callable(close): + await close() + raise RuntimeError("unreachable reset credits consume retry state") + + +async def _request_json_via_codex( + codex_client: CodexClient, + method: str, + url: str, + *, + route: ResolvedUpstreamRoute, + headers: dict[str, str], + timeout_seconds: float, + json_body: JsonObject | None = None, +) -> tuple[JsonObject, int]: + request_kwargs: dict[str, object] = { + "route": route, + "headers": headers, + "timeout": timeout_seconds, + } + if json_body is not None: + request_kwargs["json"] = json_body + resp = await codex_client.request(method, url, **request_kwargs) + return await _safe_codex_json(resp), _codex_response_status(resp) + + def build_snapshot(response: ResetCreditsResponse) -> RateLimitResetCreditsSnapshot: """Project an upstream list response into the cached snapshot shape.""" nearest = _nearest_available_expires_at(response.credits) @@ -223,6 +382,41 @@ def _consume_url(base_url: str) -> str: return f"{_reset_credits_url(base_url)}/consume" +def _codex_response_status(response: object) -> int: + value = getattr(response, "status_code", getattr(response, "status", None)) + if value is None: + return 0 + return int(value) + + +async def _safe_codex_json(response: object) -> JsonObject: + try: + json_method = getattr(response, "json", None) + if callable(json_method): + data = json_method() + if asyncio.iscoroutine(data): + data = await data + return data if isinstance(data, dict) else {"error": {"message": str(data)}} + except Exception: + pass + content = getattr(response, "content", None) + if isinstance(content, bytes): + return {"error": {"message": content.decode("utf-8", errors="replace").strip()}} + if isinstance(content, str): + return {"error": {"message": content.strip()}} + text_method = getattr(response, "text", None) + if callable(text_method): + try: + text = text_method() + if asyncio.iscoroutine(text): + text = await text + if isinstance(text, str): + return {"error": {"message": text.strip()}} + except Exception: + pass + return {"error": {"message": ""}} + + async def _safe_json(resp: aiohttp.ClientResponse) -> JsonObject: try: data = await resp.json(content_type=None) @@ -282,3 +476,7 @@ def _retry_options(attempts: int) -> ExponentialRetry: exceptions={aiohttp.ClientError, asyncio.TimeoutError}, retry_all_server_errors=False, ) + + +def _retry_delay_seconds(attempt: int) -> float: + return min(RETRY_MAX_TIMEOUT, RETRY_START_TIMEOUT * (2.0**attempt)) diff --git a/app/core/usage/reset_credits_refresh_scheduler.py b/app/core/usage/reset_credits_refresh_scheduler.py index 43dadf651..1c2ad7d41 100644 --- a/app/core/usage/reset_credits_refresh_scheduler.py +++ b/app/core/usage/reset_credits_refresh_scheduler.py @@ -13,6 +13,7 @@ ) from app.core.config.settings import get_settings from app.core.crypto import TokenEncryptor +from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route from app.db.models import Account, AccountStatus from app.db.session import get_background_session from app.modules.accounts.repository import AccountsRepository @@ -26,6 +27,7 @@ _RESET_CREDITS_SKIP_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.DEACTIVATED}) ResetCreditsFetchFn = Callable[..., Awaitable[ResetCreditsResponse]] +UpstreamRouteResolver = Callable[[Account], Awaitable[ResolvedUpstreamRoute | None]] @dataclass(slots=True) @@ -69,6 +71,7 @@ async def _refresh_once(self) -> None: encryptor=TokenEncryptor(), store=get_rate_limit_reset_credits_store(), fetch_fn=fetch_reset_credits, + route_resolver=_resolve_upstream_route_for_account, ) except Exception: logger.exception("Reset credits refresh loop failed") @@ -80,6 +83,7 @@ async def refresh_reset_credits_for_accounts( encryptor: TokenEncryptor, store: RateLimitResetCreditsStore, fetch_fn: ResetCreditsFetchFn = fetch_reset_credits, + route_resolver: UpstreamRouteResolver | None = None, ) -> None: """Refresh the cached reset-credits snapshot for each eligible account. @@ -93,7 +97,13 @@ async def refresh_reset_credits_for_accounts( continue if not account.chatgpt_account_id: continue - await _refresh_account_reset_credits(account, encryptor=encryptor, store=store, fetch_fn=fetch_fn) + await _refresh_account_reset_credits( + account, + encryptor=encryptor, + store=store, + fetch_fn=fetch_fn, + route_resolver=route_resolver, + ) async def _refresh_account_reset_credits( @@ -102,11 +112,25 @@ async def _refresh_account_reset_credits( encryptor: TokenEncryptor, store: RateLimitResetCreditsStore, fetch_fn: ResetCreditsFetchFn, + route_resolver: UpstreamRouteResolver | None, ) -> None: snapshot_generation = store.generation(account.id) try: access_token = encryptor.decrypt(account.access_token_encrypted) - response = await fetch_fn(access_token, account.chatgpt_account_id) + route = await route_resolver(account) if route_resolver is not None else None + response = await fetch_fn( + access_token, + account.chatgpt_account_id, + route=route, + allow_direct_egress=route is None, + ) + except UpstreamProxyRouteError as exc: + logger.warning( + "Reset credits route resolution failed account_id=%s reason=%s", + account.id, + exc.reason, + ) + return except Exception as exc: # scheduler must never crash the loop or mutate account status logger.warning( "Reset credits refresh failed account_id=%s error=%s", @@ -123,6 +147,16 @@ async def _refresh_account_reset_credits( ) +async def _resolve_upstream_route_for_account(account: Account) -> ResolvedUpstreamRoute | None: + async with get_background_session() as session: + return await resolve_upstream_route( + session, + account_id=account.id, + operation="reset_credits_refresh", + scope="account", + ) + + def build_rate_limit_reset_credits_scheduler() -> RateLimitResetCreditsRefreshScheduler: settings = get_settings() return RateLimitResetCreditsRefreshScheduler( diff --git a/app/modules/proxy/api.py b/app/modules/proxy/api.py index 1d53b78df..4c84eaf77 100644 --- a/app/modules/proxy/api.py +++ b/app/modules/proxy/api.py @@ -80,6 +80,7 @@ from app.core.resilience.overload import is_local_overload_error_code, merge_retry_after_headers from app.core.runtime_logging import log_error_response from app.core.types import JsonValue +from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route from app.core.utils.json_guards import is_json_mapping from app.core.utils.request_id import get_request_id from app.core.utils.sse import ( @@ -793,7 +794,7 @@ def _translate_v1_reset_credit_consume_error(exc: ConsumeResetCreditError) -> HT return HTTPException(status_code=status_code, detail=exc.message) -@v1_router.get("/reset-credit", response_model=list[V1ResetCreditEntry]) +@usage_router.get("/v1/reset-credit", response_model=list[V1ResetCreditEntry]) async def v1_reset_credit( api_key: ApiKeyData = Security(validate_usage_api_key), ) -> list[V1ResetCreditEntry]: @@ -807,7 +808,7 @@ async def v1_reset_credit( return response -@v1_router.post("/reset-credit", response_model=V1ResetCreditRedeemResponse) +@usage_router.post("/v1/reset-credit", response_model=V1ResetCreditRedeemResponse) async def v1_redeem_reset_credit( payload: V1ResetCreditRedeemRequest, api_key: ApiKeyData = Security(validate_usage_api_key), @@ -818,6 +819,10 @@ async def v1_redeem_reset_credit( raise HTTPException(status_code=403, detail="Account is outside the API key pool") if account is None: raise HTTPException(status_code=403, detail="Account is outside the API key pool") + try: + route = await _resolve_reset_credit_route(session, account.id) + except UpstreamProxyRouteError as exc: + raise HTTPException(status_code=503, detail="Unable to resolve upstream proxy route") from exc account_id = account.id access_token_encrypted = account.access_token_encrypted chatgpt_account_id = account.chatgpt_account_id @@ -829,7 +834,13 @@ async def v1_redeem_reset_credit( raise HTTPException(status_code=409, detail="Requested reset credit is unavailable") access_token = TokenEncryptor().decrypt(access_token_encrypted) try: - result = await consume_reset_credit(access_token, chatgpt_account_id, credit.id) + result = await consume_reset_credit( + access_token, + chatgpt_account_id, + credit.id, + route=route, + allow_direct_egress=route is None, + ) except ConsumeResetCreditError as exc: raise _translate_v1_reset_credit_consume_error(exc) from exc await get_rate_limit_reset_credits_store().invalidate(account_id) @@ -841,6 +852,15 @@ async def v1_redeem_reset_credit( ) +async def _resolve_reset_credit_route(session: AsyncSession, account_id: str) -> ResolvedUpstreamRoute | None: + return await resolve_upstream_route( + session, + account_id=account_id, + operation="reset_credits_consume", + scope="account", + ) + + async def _run_v1_warmup( request: Request, context: ProxyContext = Depends(get_proxy_context), diff --git a/app/modules/rate_limit_reset_credits/api.py b/app/modules/rate_limit_reset_credits/api.py index 157b728f7..19f1ea9f7 100644 --- a/app/modules/rate_limit_reset_credits/api.py +++ b/app/modules/rate_limit_reset_credits/api.py @@ -27,6 +27,7 @@ DashboardPermissionError, DashboardServiceUnavailableError, ) +from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route from app.db.models import Account from app.dependencies import AccountsContext, get_accounts_context from app.modules.rate_limit_reset_credits.store import ( @@ -93,11 +94,24 @@ async def consume_rate_limit_reset_credit( account = await context.repository.get_by_id(account_id) if account is None: raise DashboardNotFoundError("Account not found", code="account_not_found") + try: + route = await resolve_upstream_route( + context.session, + account_id=account.id, + operation="reset_credits_consume", + scope="account", + ) + except UpstreamProxyRouteError as exc: + raise DashboardServiceUnavailableError( + "Unable to resolve upstream proxy route for reset-credit consume", + code=exc.reason, + ) from exc return await _redeem_soonest_reset_credit( account=account, store=get_rate_limit_reset_credits_store(), encryptor=TokenEncryptor(), consume_fn=consume_reset_credit, + route=route, ) @@ -107,6 +121,7 @@ async def _redeem_soonest_reset_credit( store: RateLimitResetCreditsStore, encryptor: TokenEncryptor, consume_fn: ConsumeFn, + route: ResolvedUpstreamRoute | None = None, ) -> ConsumeResetCreditResponseSchema: lock = await get_reset_credit_redeem_lock(account.id) async with lock: @@ -116,7 +131,13 @@ async def _redeem_soonest_reset_credit( raise DashboardConflictError("No available reset credit", code="no_available_reset_credit") access_token = encryptor.decrypt(account.access_token_encrypted) try: - result = await consume_fn(access_token, account.chatgpt_account_id, credit.id) + result = await consume_fn( + access_token, + account.chatgpt_account_id, + credit.id, + route=route, + allow_direct_egress=route is None, + ) except ConsumeResetCreditError as exc: raise _translate_consume_error(exc) from exc redeemed_at = result.credit.redeemed_at if result.credit else None diff --git a/frontend/src/features/accounts/schemas.test.ts b/frontend/src/features/accounts/schemas.test.ts index d93fdf1aa..900755b00 100644 --- a/frontend/src/features/accounts/schemas.test.ts +++ b/frontend/src/features/accounts/schemas.test.ts @@ -214,7 +214,7 @@ describe("RateLimitResetCreditsSnapshotSchema", () => { }); describe("ConsumeRateLimitResetCreditResponseSchema", () => { - it("requires a non-null success payload", () => { + it("parses successful consume responses with optional redeemedAt", () => { expect( ConsumeRateLimitResetCreditResponseSchema.parse({ code: "rate_limit_reset", @@ -227,6 +227,28 @@ describe("ConsumeRateLimitResetCreditResponseSchema", () => { redeemedAt: ISO, }); + expect( + ConsumeRateLimitResetCreditResponseSchema.parse({ + code: "rate_limit_reset", + windowsReset: 1, + redeemedAt: null, + }), + ).toMatchObject({ + code: "rate_limit_reset", + windowsReset: 1, + redeemedAt: null, + }); + + expect( + ConsumeRateLimitResetCreditResponseSchema.parse({ + code: "rate_limit_reset", + windowsReset: 1, + }), + ).toMatchObject({ + code: "rate_limit_reset", + windowsReset: 1, + }); + expect(() => ConsumeRateLimitResetCreditResponseSchema.parse({ redeemedAt: ISO, @@ -237,7 +259,6 @@ describe("ConsumeRateLimitResetCreditResponseSchema", () => { ConsumeRateLimitResetCreditResponseSchema.parse({ code: null, windowsReset: null, - redeemedAt: null, }), ).toThrow(); }); diff --git a/frontend/src/features/accounts/schemas.ts b/frontend/src/features/accounts/schemas.ts index 7c213c221..a62af06e4 100644 --- a/frontend/src/features/accounts/schemas.ts +++ b/frontend/src/features/accounts/schemas.ts @@ -116,7 +116,7 @@ export const RateLimitResetCreditsSnapshotSchema = z.object({ export const ConsumeRateLimitResetCreditResponseSchema = z.object({ code: z.string(), windowsReset: z.number(), - redeemedAt: z.iso.datetime({ offset: true }), + redeemedAt: z.iso.datetime({ offset: true }).nullable().optional(), }); export const AccountTrendsResponseSchema = z.object({ diff --git a/tests/integration/test_v1_reset_credit.py b/tests/integration/test_v1_reset_credit.py index d0a12993b..ee66045be 100644 --- a/tests/integration/test_v1_reset_credit.py +++ b/tests/integration/test_v1_reset_credit.py @@ -123,6 +123,42 @@ async def test_v1_reset_credit_requires_valid_bearer_key(async_client): assert response.json()["error"]["code"] == "invalid_api_key" +@pytest.mark.asyncio +async def test_v1_reset_credit_accepts_valid_bearer_key_when_proxy_auth_disabled(async_client): + account_id = await _import_account( + async_client, + "acc-reset-self-service", + "self-service@example.com", + ) + _, key = await _create_api_key(async_client, name="reset-credit-self-service") + await _seed_snapshot( + account_id, + available_count=1, + credits=[ + ResetCreditItem( + id="credit-self-service", + status="available", + expires_at=datetime(2031, 1, 2, 3, 4, 5, tzinfo=timezone.utc), + ) + ], + ) + + response = await async_client.get( + "/v1/reset-credit", + headers={"Authorization": f"Bearer {key}"}, + ) + + assert response.status_code == 200 + assert response.json() == [ + { + "account_id": account_id, + "email": "self-service@example.com", + "redeem_id": "credit-self-service", + "expiredAt": "2031-01-02T03:04:05Z", + } + ] + + @pytest.mark.asyncio async def test_v1_reset_credit_scoped_pool_returns_all_available_credits_for_assigned_account(async_client): await _enable_api_key_auth(async_client) @@ -508,11 +544,20 @@ async def fake_get_lock(requested_account_id: str): assert requested_account_id == account_id return StubLock() - async def fake_consume(access_token: str, chatgpt_account_id: str, credit_id: str): + async def fake_consume( + access_token: str, + chatgpt_account_id: str, + credit_id: str, + *, + route: object | None = None, + allow_direct_egress: bool = False, + ): events.append("consume") assert access_token == "access-token" assert chatgpt_account_id == "chatgpt-session-lifecycle" assert credit_id == "credit-session-lifecycle" + assert route is None + assert allow_direct_egress is True return ConsumeResetCreditResponse.model_validate( { "code": "reset", @@ -525,8 +570,15 @@ async def fake_consume(access_token: str, chatgpt_account_id: str, credit_id: st } ) + async def fake_resolve_route(route_session, requested_account_id: str): + events.append("route_resolve") + assert route_session is session + assert requested_account_id == account_id + return None + monkeypatch.setattr("app.modules.proxy.api.get_background_session", lambda: SessionManager()) monkeypatch.setattr("app.modules.proxy.api.AccountsRepository", StubAccountsRepository) + monkeypatch.setattr("app.modules.proxy.api._resolve_reset_credit_route", fake_resolve_route) monkeypatch.setattr("app.modules.proxy.api.get_reset_credit_redeem_lock", fake_get_lock) monkeypatch.setattr("app.modules.proxy.api.consume_reset_credit", fake_consume) @@ -546,6 +598,7 @@ async def fake_consume(access_token: str, chatgpt_account_id: str, credit_id: st "session_enter", "repo_init", "repo_get", + "route_resolve", "session_exit", "lock_wait", "lock_enter", diff --git a/tests/unit/test_rate_limit_reset_credits_api.py b/tests/unit/test_rate_limit_reset_credits_api.py index 80bf2b64d..963b2f374 100644 --- a/tests/unit/test_rate_limit_reset_credits_api.py +++ b/tests/unit/test_rate_limit_reset_credits_api.py @@ -199,8 +199,23 @@ async def test_redeem_selects_soonest_calls_upstream_and_invalidates_cache() -> captured: dict[str, Any] = {} - async def consume_fn(access_token: str, account_id: str | None, credit_id: str) -> ConsumeResetCreditResponse: - captured.update({"access_token": access_token, "account_id": account_id, "credit_id": credit_id}) + async def consume_fn( + access_token: str, + account_id: str | None, + credit_id: str, + *, + route: object | None = None, + allow_direct_egress: bool = False, + ) -> ConsumeResetCreditResponse: + captured.update( + { + "access_token": access_token, + "account_id": account_id, + "credit_id": credit_id, + "route": route, + "allow_direct_egress": allow_direct_egress, + } + ) return ConsumeResetCreditResponse.model_validate( { "code": "reset", @@ -221,6 +236,8 @@ async def consume_fn(access_token: str, account_id: str | None, credit_id: str) "access_token": "decrypted-access-token", "account_id": "workspace-1", "credit_id": "soon", + "route": None, + "allow_direct_egress": True, } # Successful redemption invalidates the in-memory snapshot so the next # dashboard refresh repulls upstream state instead of serving a local edit. @@ -242,7 +259,16 @@ async def test_redeem_serializes_requests_per_account() -> None: release = asyncio.Event() consume_calls: list[str] = [] - async def consume_fn(access_token: str, account_id: str | None, credit_id: str) -> ConsumeResetCreditResponse: + async def consume_fn( + access_token: str, + account_id: str | None, + credit_id: str, + *, + route: object | None = None, + allow_direct_egress: bool = False, + ) -> ConsumeResetCreditResponse: + assert route is None + assert allow_direct_egress is True consume_calls.append(credit_id) started.set() await release.wait() @@ -303,7 +329,16 @@ async def test_redeem_translates_upstream_consume_failures( store = RateLimitResetCreditsStore() await store.set("acc_1", _snapshot([_credit("only")], available_count=1)) - async def consume_fn(access_token: str, account_id: str | None, credit_id: str) -> ConsumeResetCreditResponse: + async def consume_fn( + access_token: str, + account_id: str | None, + credit_id: str, + *, + route: object | None = None, + allow_direct_egress: bool = False, + ) -> ConsumeResetCreditResponse: + assert route is None + assert allow_direct_egress is True raise ConsumeResetCreditError(status_code, f"upstream failed {status_code}", code=f"upstream_{status_code}") with pytest.raises(expected_exception) as excinfo: diff --git a/tests/unit/test_rate_limit_reset_credits_client.py b/tests/unit/test_rate_limit_reset_credits_client.py index 4c9913226..9a7342701 100644 --- a/tests/unit/test_rate_limit_reset_credits_client.py +++ b/tests/unit/test_rate_limit_reset_credits_client.py @@ -17,6 +17,7 @@ fetch_reset_credits, ) from app.core.clients.usage import _usage_headers +from app.core.upstream_proxy import ResolvedProxyEndpoint, ResolvedUpstreamRoute pytestmark = pytest.mark.unit @@ -112,6 +113,24 @@ def request( ) +class StubCodexClient: + def __init__(self, response: object) -> None: + self.response = response + self.calls: list[dict[str, Any]] = [] + + async def request(self, method: str, url: str, **kwargs: Any) -> object: + self.calls.append({"method": method, "url": url, **kwargs}) + return self.response + + +def _route() -> ResolvedUpstreamRoute: + return ResolvedUpstreamRoute( + mode="account_bound", + pool_id="pool_1", + endpoint=ResolvedProxyEndpoint(id="endpoint_1", scheme="http", host="proxy.local", port=8080), + ) + + def _list_payload() -> dict: return { "credits": [ @@ -148,6 +167,7 @@ async def test_fetch_reset_credits_sends_bearer_and_account_id_headers() -> None timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert isinstance(data, ResetCreditsResponse) @@ -160,6 +180,29 @@ async def test_fetch_reset_credits_sends_bearer_and_account_id_headers() -> None assert state.headers["chatgpt-account-id"] == "acc_workspace" +@pytest.mark.asyncio +async def test_fetch_reset_credits_uses_resolved_route_when_provided() -> None: + route = _route() + codex_client = StubCodexClient(StubResponse(200, _list_payload(), "")) + + data = await fetch_reset_credits( + "access-token", + "acc_workspace", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + max_retries=0, + route=route, + codex_client=cast(Any, codex_client), + ) + + assert data.available_count == 1 + assert len(codex_client.calls) == 1 + call = codex_client.calls[0] + assert call["method"] == "GET" + assert call["route"] is route + assert call["url"] == "http://upstream.test/backend-api/wham/rate-limit-reset-credits" + + @pytest.mark.asyncio async def test_fetch_reset_credits_skips_account_id_header_for_email_and_local_prefixes() -> None: for account_id in ("email_user@example.com", "local_abcd"): @@ -172,6 +215,7 @@ async def test_fetch_reset_credits_skips_account_id_header_for_email_and_local_p timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert state.headers is not None assert "chatgpt-account-id" not in state.headers, account_id @@ -189,6 +233,7 @@ async def test_fetch_reset_credits_normalizes_base_url_without_backend_api_segme timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert state.url == "http://upstream.test/backend-api/wham/rate-limit-reset-credits" @@ -210,6 +255,7 @@ async def test_fetch_reset_credits_raises_on_non_200() -> None: timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 401 @@ -229,6 +275,7 @@ async def test_fetch_reset_credits_handles_non_json_body() -> None: timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 502 @@ -247,6 +294,7 @@ async def test_fetch_reset_credits_rejects_malformed_success_body() -> None: timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 502 @@ -265,6 +313,7 @@ async def test_fetch_reset_credits_rejects_success_body_missing_contract_fields( timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 502 @@ -300,6 +349,7 @@ async def test_consume_reset_credit_sends_credit_id_and_uuid_redeem_request_id() timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert isinstance(result, ConsumeResetCreditResponse) @@ -322,6 +372,39 @@ async def test_consume_reset_credit_sends_credit_id_and_uuid_redeem_request_id() assert str(parsed) == redeem_request_id +@pytest.mark.asyncio +async def test_consume_reset_credit_uses_resolved_route_when_provided() -> None: + route = _route() + codex_client = StubCodexClient( + StubResponse( + 200, + { + "code": "reset", + "credit": {"id": "RateLimitResetCredit_test", "status": "redeemed"}, + "windows_reset": 1, + }, + "", + ) + ) + + result = await consume_reset_credit( + "access-token", + "acc_workspace", + "RateLimitResetCredit_test", + base_url="http://upstream.test/backend-api", + timeout_seconds=2.0, + route=route, + codex_client=cast(Any, codex_client), + ) + + assert result.windows_reset == 1 + assert len(codex_client.calls) == 1 + call = codex_client.calls[0] + assert call["method"] == "POST" + assert call["route"] is route + assert call["json"]["credit_id"] == "RateLimitResetCredit_test" + + @pytest.mark.asyncio async def test_consume_reset_credit_generates_fresh_redeem_request_id_each_call() -> None: ids: list[str] = [] @@ -339,6 +422,7 @@ async def test_consume_reset_credit_generates_fresh_redeem_request_id_each_call( timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert state.json_body is not None ids.append(state.json_body["redeem_request_id"]) @@ -362,6 +446,7 @@ async def test_consume_reset_credit_raises_on_non_200() -> None: timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 409 @@ -382,6 +467,7 @@ async def test_consume_reset_credit_rejects_malformed_success_body() -> None: timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 502 @@ -401,6 +487,7 @@ async def test_consume_reset_credit_rejects_success_body_missing_contract_fields timeout_seconds=2.0, max_retries=0, client=cast(Any, client), + allow_direct_egress=True, ) assert excinfo.value.status_code == 502 diff --git a/tests/unit/test_rate_limit_reset_credits_scheduler.py b/tests/unit/test_rate_limit_reset_credits_scheduler.py index a2e893613..c9371a26e 100644 --- a/tests/unit/test_rate_limit_reset_credits_scheduler.py +++ b/tests/unit/test_rate_limit_reset_credits_scheduler.py @@ -68,7 +68,7 @@ async def test_refresh_skips_paused_and_deactivated_accounts() -> None: store = RateLimitResetCreditsStore() fetched: list[str] = [] - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: fetched.append(access_token) return _response() @@ -97,7 +97,7 @@ async def test_refresh_skips_account_without_chatgpt_account_id() -> None: store = RateLimitResetCreditsStore() fetched: list[str] = [] - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: fetched.append(access_token) return _response() @@ -117,7 +117,7 @@ async def test_one_account_failure_does_not_break_the_loop() -> None: store = RateLimitResetCreditsStore() fetched: list[str] = [] - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: fetched.append(access_token) if access_token == "token-for-acc_fail": raise ResetCreditFetchError(500, "boom") @@ -148,7 +148,7 @@ async def test_upstream_error_retains_prior_snapshot_and_does_not_mutate_status( await store.set("acc_retain", prior) account = _make_account("acc_retain", status=AccountStatus.ACTIVE) - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: raise ResetCreditFetchError(503, "busy") await refresh_reset_credits_for_accounts( @@ -174,7 +174,7 @@ async def test_refresh_does_not_resurrect_snapshot_invalidated_during_fetch() -> fetch_started = asyncio.Event() release_fetch = asyncio.Event() - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: fetch_started.set() await release_fetch.wait() return _response(available_count=1) @@ -204,7 +204,7 @@ async def test_unrelated_account_write_does_not_drop_in_flight_refresh() -> None fetch_started = asyncio.Event() release_fetch = asyncio.Event() - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: fetch_started.set() await release_fetch.wait() return _response(available_count=4) @@ -239,7 +239,7 @@ async def test_refresh_never_calls_account_status_writes() -> None: """ store = RateLimitResetCreditsStore() - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: if access_token == "token-for-acc_fail": raise ResetCreditFetchError(401, "unauthorized") return _response() @@ -287,8 +287,9 @@ async def _fake_background_session(): monkeypatch.setattr(scheduler_module, "AccountsRepository", lambda session: _FakeRepo()) monkeypatch.setattr(scheduler_module, "TokenEncryptor", lambda: StubEncryptor()) monkeypatch.setattr(scheduler_module, "get_rate_limit_reset_credits_store", lambda: store) + monkeypatch.setattr(scheduler_module, "_resolve_upstream_route_for_account", lambda account: _async_none()) - async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsResponse: + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: captured.append(("fetch", access_token, account_id)) return _response(available_count=7) @@ -302,3 +303,7 @@ async def fetch_fn(access_token: str, account_id: str | None) -> ResetCreditsRes assert snapshot is not None assert snapshot.available_count == 7 assert account.status == AccountStatus.ACTIVE + + +async def _async_none() -> None: + return None From 363bd3656537df419ee68595c963a85e231d9623 Mon Sep 17 00:00:00 2001 From: Jacky Fong Date: Sat, 20 Jun 2026 02:24:15 +0800 Subject: [PATCH 09/39] fix(reset-credits): invalidate stale snapshots for ineligible accounts --- .../usage/reset_credits_refresh_scheduler.py | 9 ++- app/modules/accounts/mappers.py | 12 +++- app/modules/rate_limit_reset_credits/api.py | 16 ++++- .../add-rate-limit-reset-credits/design.md | 5 +- .../specs/frontend-architecture/spec.md | 8 +-- .../specs/rate-limit-reset-credits/context.md | 13 ++-- .../specs/rate-limit-reset-credits/spec.md | 26 +++++-- .../add-rate-limit-reset-credits/tasks.md | 4 +- .../unit/test_rate_limit_reset_credits_api.py | 68 +++++++++++++++++++ .../test_rate_limit_reset_credits_mapper.py | 47 ++++++++++++- ...test_rate_limit_reset_credits_scheduler.py | 44 ++++++++++++ 11 files changed, 227 insertions(+), 25 deletions(-) diff --git a/app/core/usage/reset_credits_refresh_scheduler.py b/app/core/usage/reset_credits_refresh_scheduler.py index 1c2ad7d41..093f985a4 100644 --- a/app/core/usage/reset_credits_refresh_scheduler.py +++ b/app/core/usage/reset_credits_refresh_scheduler.py @@ -93,9 +93,8 @@ async def refresh_reset_credits_for_accounts( stays owned by usage refresh. One account failing must not abort the loop. """ for account in accounts: - if account.status in _RESET_CREDITS_SKIP_STATUSES: - continue - if not account.chatgpt_account_id: + if not _is_reset_credits_refresh_eligible(account): + await store.invalidate(account.id) continue await _refresh_account_reset_credits( account, @@ -106,6 +105,10 @@ async def refresh_reset_credits_for_accounts( ) +def _is_reset_credits_refresh_eligible(account: Account) -> bool: + return account.status not in _RESET_CREDITS_SKIP_STATUSES and bool(account.chatgpt_account_id) + + async def _refresh_account_reset_credits( account: Account, *, diff --git a/app/modules/accounts/mappers.py b/app/modules/accounts/mappers.py index ee07bf840..2c751502c 100644 --- a/app/modules/accounts/mappers.py +++ b/app/modules/accounts/mappers.py @@ -30,6 +30,7 @@ from app.modules.usage.mappers import usage_history_to_window_row _ACCOUNT_ROUTING_POLICIES = frozenset({"burn_first", "normal", "preserve"}) +_RESET_CREDITS_INELIGIBLE_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.DEACTIVATED}) _DEFAULT_USAGE_REFRESH_INTERVAL_SECONDS = 60 @@ -60,7 +61,7 @@ def build_account_summaries( encryptor, include_auth=include_auth, is_email_duplicate=_duplicate_detection_key(account) in duplicate_keys, - reset_credits_snapshot=store.get(account.id), + reset_credits_snapshot=_reset_credits_snapshot_for_account(account, store), ) for account in accounts ] @@ -281,6 +282,15 @@ def _normalize_account_routing_policy(value: str | None) -> str: return "normal" +def _reset_credits_snapshot_for_account( + account: Account, + store: RateLimitResetCreditsStore, +) -> RateLimitResetCreditsSnapshot | None: + if account.status in _RESET_CREDITS_INELIGIBLE_STATUSES or not account.chatgpt_account_id: + return None + return store.get(account.id) + + def _limit_warmup_to_status(entry: AccountLimitWarmup | None) -> AccountLimitWarmupStatus | None: if entry is None: return None diff --git a/app/modules/rate_limit_reset_credits/api.py b/app/modules/rate_limit_reset_credits/api.py index 19f1ea9f7..3adffb1b7 100644 --- a/app/modules/rate_limit_reset_credits/api.py +++ b/app/modules/rate_limit_reset_credits/api.py @@ -28,7 +28,7 @@ DashboardServiceUnavailableError, ) from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route -from app.db.models import Account +from app.db.models import Account, AccountStatus from app.dependencies import AccountsContext, get_accounts_context from app.modules.rate_limit_reset_credits.store import ( RateLimitResetCreditsStore, @@ -45,6 +45,7 @@ ConsumeFn = Callable[..., Awaitable[ConsumeResetCreditResponse]] _redeem_locks: dict[str, asyncio.Lock] = {} _redeem_locks_registry_lock = asyncio.Lock() +_RESET_CREDITS_INELIGIBLE_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.DEACTIVATED}) class ResetCreditItemResponse(DashboardModel): @@ -94,6 +95,13 @@ async def consume_rate_limit_reset_credit( account = await context.repository.get_by_id(account_id) if account is None: raise DashboardNotFoundError("Account not found", code="account_not_found") + store = get_rate_limit_reset_credits_store() + if not _account_can_redeem_reset_credit(account): + await store.invalidate(account.id) + raise DashboardConflictError( + "Account is not eligible to redeem reset credits", + code="reset_credit_account_ineligible", + ) try: route = await resolve_upstream_route( context.session, @@ -108,7 +116,7 @@ async def consume_rate_limit_reset_credit( ) from exc return await _redeem_soonest_reset_credit( account=account, - store=get_rate_limit_reset_credits_store(), + store=store, encryptor=TokenEncryptor(), consume_fn=consume_reset_credit, route=route, @@ -149,6 +157,10 @@ async def _redeem_soonest_reset_credit( ) +def _account_can_redeem_reset_credit(account: Account) -> bool: + return account.status not in _RESET_CREDITS_INELIGIBLE_STATUSES and bool(account.chatgpt_account_id) + + async def get_reset_credit_redeem_lock(account_id: str) -> asyncio.Lock: lock = _redeem_locks.get(account_id) if lock is not None: diff --git a/openspec/changes/add-rate-limit-reset-credits/design.md b/openspec/changes/add-rate-limit-reset-credits/design.md index 5840d2d9a..fc8417025 100644 --- a/openspec/changes/add-rate-limit-reset-credits/design.md +++ b/openspec/changes/add-rate-limit-reset-credits/design.md @@ -50,7 +50,7 @@ The reference is a single-account CLI; codex-lb needs a multi-account, dashboard **Alternatives considered:** Separate `/api/accounts/{id}/rate-limit-reset-credits` GET consumed per-card — rejected because it adds N round-trips and N re-renders; the count belongs on the summary the UI already fetches. ### Decision: Countdown is single-unit and goes red under 7 days -**Rationale:** User requirement: one unit only ("6d" / "13h" / "45m" / "now"), red when `< 7d`. A new `formatSingleUnitRemaining(expiresAtIso)` helper sits next to existing `formatQuotaResetLabel` / `formatResetRelative` in `utils/formatters.ts`; the caller colors it via `ms < 7 * DAY_MS`. We do NOT add a ticking hook (matches the existing reset-label pattern). The confirmation dialog uses a separate local-time formatter with exact `YYYY-MM-DD HH:MM:SS` output so operators can see the precise expiry instant in their own timezone. +**Rationale:** User requirement: one unit only ("6d" / "13h" / "45m" / "now"), red when `< 7d`. A new `formatSingleUnitRemaining(expiresAtIso)` helper sits next to existing `formatQuotaResetLabel` / `formatResetRelative` in `utils/formatters.ts`; the caller colors it via `ms < 7 * DAY_MS`. We do NOT add a ticking hook (matches the existing reset-label pattern). The confirmation dialog remains generic and does not need to render the upstream credit title, description, exact expiry timestamp, or upstream partial-reset consumption caveat. **Alternatives considered:** Reuse `formatResetRelative` — rejected because it returns multi-unit ("6d 13h") output. ### Decision: Reset credit refresh never mutates account status @@ -61,9 +61,10 @@ The reference is a single-account CLI; codex-lb needs a multi-account, dashboard - **[Upstream endpoints are undocumented]** → Mitigation: client treats non-200 / non-JSON defensively, logs, keeps prior snapshot; consume-failure surfaces to UI as a toast without invalidating the cache. Document the upstream-dependence caveat in the capability `context.md`. - **[In-memory cache lost on restart]** → Mitigation: acceptable per requirements; the next 60s tick repopulates. UI treats missing snapshot as `available_reset_credits: 0` (hidden affordances), not an error. -- **[Credit consumed even on partial reset]** (upstream behavior: "if POST returns 200, the credit is gone") → Mitigation: confirmation dialog explicitly warns that the credit is consumed regardless of whether the rate-limit window moves. On success we invalidate the cache and let the next tick reconcile. +- **[Credit consumed even on partial reset]** (upstream behavior: "if POST returns 200, the credit is gone") → Mitigation: on success we invalidate the cache and let the next tick reconcile. This caveat is documented in OpenSpec context but is not required in the dashboard confirmation dialog. - **[Race: credit expires between render and click]** → Mitigation: server re-selects from the freshest cached snapshot at consume time and surfaces upstream's error if the chosen credit is no longer redeemable. - **[Race: `/v1/reset-credit` client redeems a stale `redeem_id`]** → Mitigation: the POST handler re-reads the current cached snapshot, rejects credits that are no longer `available`, and only forwards the exact `redeem_id` when it still matches an available credit on an in-pool account. +- **[Disabled or incomplete account keeps stale cached credits]** → Mitigation: paused, deactivated, and missing-`chatgpt-account-id` accounts invalidate any existing snapshot during scheduler refresh. Account summaries suppress such snapshots immediately, and dashboard consume refuses them before route resolution or upstream calls. - **[Many accounts = many upstream calls per tick]** → Mitigation: reuse the same skip rules (paused/deactivated/missing chatgpt-account-id) and keep the interval configurable. Each replica polls so its process-local cache is useful for dashboard reads; moving snapshots to shared storage can later reduce duplicate polling if upstream load becomes a problem. - **[Guest read-only dashboard users]** → Mitigation: `POST /consume` requires `require_dashboard_write_access`; guests can see the badge/button (count is read off `AccountSummary`) but cannot redeem. diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md index 836f4b0ce..5e2721f41 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/frontend-architecture/spec.md @@ -2,7 +2,7 @@ ### Requirement: Accounts page exposes a reset-credits redeem action -The Accounts page per-account action bar SHALL render a `Reset (N)` button next to the existing Export button with matching button styling whenever the account reports `available_reset_credits > 0`, where `N` is the available reset-credit count for that account. The button SHALL be hidden when `available_reset_credits` is `0`. Activating the button SHALL open a confirmation dialog that names the soonest-expiring credit's title, shows its expiry in local time using `YYYY-MM-DD HH:MM:SS`, and explicitly warns that the credit is consumed regardless of whether the rate-limit window moves. Confirming SHALL submit a redeem request for that account and refresh account data on success. +The Accounts page per-account action bar SHALL render a `Reset (N)` button next to the existing Export button with matching button styling whenever the account reports `available_reset_credits > 0`, where `N` is the available reset-credit count for that account. The button SHALL be hidden when `available_reset_credits` is `0`. Activating the button SHALL open a confirmation dialog that explains the dashboard will redeem the soonest-expiring banked reset credit for this account. Confirming SHALL submit a redeem request for that account and refresh account data on success. #### Scenario: Reset button mirrors Export styling and placement - **WHEN** the Accounts page renders the per-account action bar for an account with `available_reset_credits > 0` @@ -15,12 +15,12 @@ The Accounts page per-account action bar SHALL render a `Reset (N)` button next #### Scenario: Confirmation required before redeem - **WHEN** the operator clicks the "Reset" button -- **THEN** a confirmation dialog opens describing the soonest-expiring credit and the no-refund warning +- **THEN** a confirmation dialog opens describing the soonest-expiring reset-credit redemption - **AND** no redeem request is sent until the operator confirms -#### Scenario: Confirmation dialog shows local expiry timestamp +#### Scenario: Confirmation dialog can remain generic - **WHEN** the operator opens the reset-credit confirmation dialog -- **THEN** the dialog renders the credit expiry in local time using `YYYY-MM-DD HH:MM:SS` +- **THEN** the dialog is not required to render the upstream credit title, description, expiry timestamp, or upstream partial-reset consumption warning ### Requirement: AccountListItem displays a reset-credits count badge diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md index 2f311682b..da2b27e63 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/context.md @@ -32,6 +32,10 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. - **In-memory only.** No DB column, no migration. Each replica refreshes its own process-local snapshots, which repopulate within one tick of startup. Restart cost: up to 60s of `available_reset_credits: 0` on that replica. +- **Eligibility clears stale snapshots.** Paused accounts, deactivated accounts, and accounts + without a usable `chatgpt-account-id` do not fetch reset credits. If they already have a + cached snapshot, the scheduler invalidates it; account summaries suppress it immediately; + and dashboard consume rejects the account before route resolution or upstream calls. - **Server picks the credit, not the client.** `POST /consume` takes only the account id; the server selects the soonest-expiring available credit from the freshest snapshot and generates the `redeem_request_id`. Avoids stale-UI and clock-skew races. @@ -55,8 +59,9 @@ client treats non-200, non-JSON, and schema-drifted 200 responses defensively. ## Failure Modes - **Upstream returns 200 but the rate-limit window doesn't move.** Per upstream behavior - the credit is still consumed. The confirmation dialog warns the operator; on success we - invalidate the cache and let the next tick reconcile `available_count`. + the credit is still consumed. This is an upstream caveat rather than a dashboard dialog + requirement; on success we invalidate the cache and let the next tick reconcile + `available_count`. - **Snapshot is empty/stale.** UI hides all reset affordances for that account (`available_reset_credits: 0`). Not an error — wait one tick. - **Upstream 401/403/auth-expired.** Logged; prior snapshot retained. Does NOT deactivate @@ -153,8 +158,8 @@ with `null` expiries last, so the response remains deterministic for the same el - The 60s cadence matches usage refresh, but each replica polls because each replica serves dashboard reads from its own process-local snapshot cache. -- A credit is consumed as soon as upstream returns 200 — treat the confirmation dialog as - the point of no return. +- A credit is consumed as soon as upstream returns 200; operators should treat the confirm + action as the point of no return. - `/v1/reset-credit` uses the same process-local snapshot cache as the dashboard flow, so a client may need to retry after the next refresh tick if an account has just restarted or recently redeemed a credit. diff --git a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md index 0632293fa..e2b07e813 100644 --- a/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md +++ b/openspec/changes/add-rate-limit-reset-credits/specs/rate-limit-reset-credits/spec.md @@ -2,7 +2,7 @@ ### Requirement: Reset credits are polled per account on a fixed cadence -The system SHALL poll upstream `GET /wham/rate-limit-reset-credits` for each eligible account on a configurable cadence that defaults to 60 seconds, using that account's stored OAuth bearer token and `chatgpt-account-id`. The scheduler SHALL always start with the application lifespan. Because snapshots are kept in process-local memory, every running replica SHALL refresh its own snapshot cache instead of relying on leader election. The poll SHALL skip any account that is paused, deactivated, or lacks a usable `chatgpt-account-id`. +The system SHALL poll upstream `GET /wham/rate-limit-reset-credits` for each eligible account on a configurable cadence that defaults to 60 seconds, using that account's stored OAuth bearer token and `chatgpt-account-id`. The scheduler SHALL always start with the application lifespan. Because snapshots are kept in process-local memory, every running replica SHALL refresh its own snapshot cache instead of relying on leader election. The poll SHALL skip any account that is paused, deactivated, or lacks a usable `chatgpt-account-id`, and SHALL invalidate any cached reset-credit snapshot for skipped accounts. #### Scenario: Default cadence polls every 60 seconds - **WHEN** the application starts with default settings @@ -16,11 +16,16 @@ The system SHALL poll upstream `GET /wham/rate-limit-reset-credits` for each eli #### Scenario: Paused and deactivated accounts are skipped - **WHEN** an account is persisted as `paused` or `deactivated` - **THEN** the scheduler performs no upstream reset-credits fetch for that account -- **AND** the cached snapshot for that account (if any) is left untouched by the skip +- **AND** the cached snapshot for that account (if any) is invalidated + +#### Scenario: Account without ChatGPT account id is skipped +- **WHEN** an account lacks a usable `chatgpt-account-id` +- **THEN** the scheduler performs no upstream reset-credits fetch for that account +- **AND** the cached snapshot for that account (if any) is invalidated ### Requirement: Reset credit snapshots are cached in memory keyed by account -The system SHALL store the most recent successful reset-credits response per account in an in-memory store keyed by account id. The store SHALL be concurrency-safe and SHALL provide an `invalidate(account_id)` operation. Account-summary mappers SHALL join the cached snapshot onto each account summary, exposing `available_reset_credits` (integer) and `reset_credit_nearest_expires_at` (ISO timestamp or null). Accounts with no cached snapshot SHALL expose `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null`. +The system SHALL store the most recent successful reset-credits response per account in an in-memory store keyed by account id. The store SHALL be concurrency-safe and SHALL provide an `invalidate(account_id)` operation. Account-summary mappers SHALL join the cached snapshot onto each eligible account summary, exposing `available_reset_credits` (integer) and `reset_credit_nearest_expires_at` (ISO timestamp or null). Accounts with no cached snapshot, accounts that are paused or deactivated, and accounts without a usable `chatgpt-account-id` SHALL expose `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null`. #### Scenario: Account summary reflects cached credits - **GIVEN** an account has a cached reset-credits snapshot with `available_count: 2` and a soonest expiry of `2026-07-10T00:00:00Z` @@ -32,6 +37,12 @@ The system SHALL store the most recent successful reset-credits response per acc - **WHEN** the account-summary mapper builds the summary for that account - **THEN** the summary exposes `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null` +#### Scenario: Ineligible account summary suppresses stale credits +- **GIVEN** an account is paused, deactivated, or lacks a usable `chatgpt-account-id` +- **AND** a cached reset-credits snapshot still exists for that account +- **WHEN** the account-summary mapper builds the summary for that account +- **THEN** the summary exposes `available_reset_credits: 0` and `reset_credit_nearest_expires_at: null` + #### Scenario: Invalidate forces re-fetch on next tick - **WHEN** a caller invokes `invalidate(account_id)` for an account - **THEN** subsequent reads for that account return no cached snapshot @@ -45,7 +56,7 @@ The system SHALL store the most recent successful reset-credits response per acc ### Requirement: Operators can redeem the soonest-expiring available credit -The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` that redeems exactly one credit for the named account. The endpoint SHALL select, from the freshest cached snapshot, the credit whose `status` is `available` with the smallest `expires_at`, generate a `redeem_request_id` (UUID v4), and forward `{credit_id, redeem_request_id}` to upstream `POST /wham/rate-limit-reset-credits/consume` using the account's bearer token and `chatgpt-account-id`. A cached snapshot with `available_count <= 0` MUST be treated as having no redeemable credits, even if the cached `credits` list contains an item marked `available`. On a 200 response the endpoint SHALL invalidate the cached snapshot for that account and return `{code, windows_reset, redeemed_at}`. The endpoint SHALL require dashboard write access; read-only guests MUST be refused. +The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/rate-limit-reset-credits/consume` that redeems exactly one credit for the named account. The endpoint SHALL refuse paused accounts, deactivated accounts, and accounts without a usable `chatgpt-account-id`, invalidating any cached snapshot for the account before returning a client error. For eligible accounts, the endpoint SHALL select, from the freshest cached snapshot, the credit whose `status` is `available` with the smallest `expires_at`, generate a `redeem_request_id` (UUID v4), and forward `{credit_id, redeem_request_id}` to upstream `POST /wham/rate-limit-reset-credits/consume` using the account's bearer token and `chatgpt-account-id`. A cached snapshot with `available_count <= 0` MUST be treated as having no redeemable credits, even if the cached `credits` list contains an item marked `available`. On a 200 response the endpoint SHALL invalidate the cached snapshot for that account and return `{code, windows_reset, redeemed_at}`. The endpoint SHALL require dashboard write access; read-only guests MUST be refused. #### Scenario: Consume selects the soonest-expiring credit - **GIVEN** an account has cached credits with expiries `2026-07-10Z` and `2026-06-20Z`, both `status: available` @@ -80,6 +91,13 @@ The system SHALL expose a dashboard endpoint `POST /api/accounts/{account_id}/ra - **WHEN** the operator invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` - **THEN** the endpoint returns a `409` (or equivalent client-error) without calling upstream +#### Scenario: Consume refuses ineligible account before upstream call +- **GIVEN** an account is paused, deactivated, or lacks a usable `chatgpt-account-id` +- **AND** a cached reset-credits snapshot still exists for that account +- **WHEN** the operator invokes `POST /api/accounts/{id}/rate-limit-reset-credits/consume` +- **THEN** the endpoint invalidates the cached snapshot for that account +- **AND** returns a client error without resolving a route or calling upstream + ### Requirement: API-key self-service reset-credit reads and exact redemption reuse the cached snapshots The system SHALL expose `GET /v1/reset-credit` and `POST /v1/reset-credit` as API-key-authenticated self-service routes backed by the same cached reset-credit snapshots used by the dashboard flow. `GET /v1/reset-credit` SHALL project the authenticated API key's eligible account pool into one array item per available credit, ordered by account email ascending, then account id ascending, then credit `expires_at` ascending with `null` expiries last, then credit id ascending. Each item SHALL include `account_id`, `email`, `redeem_id`, and `expiredAt`, where `redeem_id` and `expiredAt` come from that available credit. Accounts with no cached snapshot or no available credit SHALL be omitted from the `GET` response. diff --git a/openspec/changes/add-rate-limit-reset-credits/tasks.md b/openspec/changes/add-rate-limit-reset-credits/tasks.md index 7a45451b0..bacf74315 100644 --- a/openspec/changes/add-rate-limit-reset-credits/tasks.md +++ b/openspec/changes/add-rate-limit-reset-credits/tasks.md @@ -29,7 +29,7 @@ - [x] 4.2 Add the `Reset (N)` button to `frontend/src/features/accounts/components/account-actions.tsx` immediately after the Export button, matching its `size="sm" variant="outline" className="h-8 gap-1.5 text-xs"` style, with a `RotateCcw` icon, a single-unit countdown label (using 3.3) placed at the button's right-upper radius, and destructive/red label color when `expiringSoon`. Render only when `availableResetCredits > 0`. Wire `onClick` to open the confirmation dialog - [x] 4.3 Add a reset action to `frontend/src/features/dashboard/components/account-list.tsx` (table view) inside the existing Details action cell, matching the `h-7 w-7` icon-button style with the countdown and count exposed in the `title` tooltip. Render only when `availableResetCredits > 0` - [x] 4.4 Add a `Reset (N)` button to `frontend/src/features/dashboard/components/account-card.tsx` (grid view) next to the Details button, matching the `h-7 gap-1.5` text style with the single-unit countdown label. Render only when `availableResetCredits > 0` -- [x] 4.5 Implement the confirmation dialog (reuse `frontend/src/components/confirm-dialog.tsx` + `frontend/src/hooks/use-dialog-state.ts`, same shape as the delete-account dialog): body shows the soonest credit's title and `expires_at` formatted as local `YYYY-MM-DD HH:MM:SS`, plus the "credit is consumed even if the window doesn't move" warning. On confirm → call `consumeRateLimitResetCredit(accountId)` → success/failure toast → query invalidation +- [x] 4.5 Implement the confirmation dialog (reuse `frontend/src/components/confirm-dialog.tsx` + `frontend/src/hooks/use-dialog-state.ts`, same shape as the delete-account dialog): body explains that the dashboard redeems the soonest-expiring banked reset credit for the account. On confirm → call `consumeRateLimitResetCredit(accountId)` → success/failure toast → query invalidation - [x] 4.6 Add a "Most reset credits" option to the Accounts page sort selector in `frontend/src/features/accounts/sorting.ts` and make it the default Accounts page sort mode: comparator orders by `availableResetCredits` desc, tiebreak by `resetCreditNearestExpiresAt` asc (soonest first), accounts with null expiry last. Add the localized dropdown label - [x] 4.7 Add a summed reset-credit badge to `frontend/src/components/layout/app-header.tsx` for the Accounts nav tab, capped at `99+` @@ -43,7 +43,7 @@ - [x] 5.6 Frontend — `formatSingleUnitRemaining`: boundaries at 7d (color flip), 1d, 1h, 1m, and `now`; sub-minute and past timestamps both yield `"now"` - [x] 5.7 Frontend — `AccountListItem` badge: renders count, `"99+"` at 100+, absent at 0 - [x] 5.8 Frontend — Reset button visibility: rendered when `availableResetCredits > 0`, absent at 0, in all three surfaces (account-actions, dashboard table, dashboard grid) -- [x] 5.9 Frontend — confirm dialog → consume: confirmation calls `consumeRateLimitResetCredit`, shows the expiry in local `YYYY-MM-DD HH:MM:SS`, success path invalidates queries, failure path surfaces a toast and does not invalidate +- [x] 5.9 Frontend — confirm dialog → consume: confirmation calls `consumeRateLimitResetCredit`, success path invalidates queries, failure path surfaces a toast and does not invalidate - [x] 5.10 Frontend — "Most reset credits" sort: comparator orders by count desc with soonest-expiry tiebreak, null-expiry accounts last - [x] 5.11 Frontend — Accounts nav badge: shows the summed total, caps at `99+`, and hides at zero - [x] 5.12 Backend — `/v1/reset-credit` GET: requires Bearer API key, honors scoped vs unscoped account pools, emits a deterministic array with `email` in each object for every available credit, and omits accounts without available cached credits diff --git a/tests/unit/test_rate_limit_reset_credits_api.py b/tests/unit/test_rate_limit_reset_credits_api.py index 963b2f374..27dcb53be 100644 --- a/tests/unit/test_rate_limit_reset_credits_api.py +++ b/tests/unit/test_rate_limit_reset_credits_api.py @@ -59,6 +59,18 @@ def _account(account_id: str = "acc_1") -> Account: ) +def _account_with_state( + account_id: str, + *, + status: AccountStatus = AccountStatus.ACTIVE, + chatgpt_account_id: str | None = "workspace-1", +) -> Account: + account = _account(account_id) + account.status = status + account.chatgpt_account_id = chatgpt_account_id + return account + + def _credit( credit_id: str, *, @@ -373,6 +385,62 @@ async def get_by_id(self, account_id: str) -> Account | None: ) +@pytest.mark.asyncio +@pytest.mark.parametrize("status", [AccountStatus.PAUSED, AccountStatus.DEACTIVATED]) +async def test_consume_handler_rejects_ineligible_account_status_and_invalidates_snapshot( + monkeypatch: pytest.MonkeyPatch, + status: AccountStatus, +) -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_disabled", _snapshot([_credit("stale")], available_count=1)) + + class _Repo: + async def get_by_id(self, account_id: str) -> Account | None: + return _account_with_state(account_id, status=status) + + async def _route_not_called(*args: Any, **kwargs: Any) -> object: + raise AssertionError("ineligible accounts must be rejected before route resolution") + + monkeypatch.setattr(reset_credits_api, "get_rate_limit_reset_credits_store", lambda: store) + monkeypatch.setattr(reset_credits_api, "resolve_upstream_route", _route_not_called) + fake_context = SimpleNamespace(repository=_Repo()) + + with pytest.raises(DashboardConflictError) as excinfo: + await consume_rate_limit_reset_credit( + account_id="acc_disabled", + _write_access=None, + context=cast(Any, fake_context), + ) + + assert excinfo.value.code == "reset_credit_account_ineligible" + assert store.get("acc_disabled") is None + + +@pytest.mark.asyncio +async def test_consume_handler_rejects_account_without_chatgpt_account_id_and_invalidates_snapshot( + monkeypatch: pytest.MonkeyPatch, +) -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_no_workspace", _snapshot([_credit("stale")], available_count=1)) + + class _Repo: + async def get_by_id(self, account_id: str) -> Account | None: + return _account_with_state(account_id, chatgpt_account_id=None) + + monkeypatch.setattr(reset_credits_api, "get_rate_limit_reset_credits_store", lambda: store) + fake_context = SimpleNamespace(repository=_Repo()) + + with pytest.raises(DashboardConflictError) as excinfo: + await consume_rate_limit_reset_credit( + account_id="acc_no_workspace", + _write_access=None, + context=cast(Any, fake_context), + ) + + assert excinfo.value.code == "reset_credit_account_ineligible" + assert store.get("acc_no_workspace") is None + + # --- POST consume: write-access gating refuses guests (full ASGI path) --- diff --git a/tests/unit/test_rate_limit_reset_credits_mapper.py b/tests/unit/test_rate_limit_reset_credits_mapper.py index 8774bc48f..15225d34e 100644 --- a/tests/unit/test_rate_limit_reset_credits_mapper.py +++ b/tests/unit/test_rate_limit_reset_credits_mapper.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import cast import pytest @@ -12,18 +13,29 @@ pytestmark = pytest.mark.unit +_DEFAULT_CHATGPT_ACCOUNT_ID = object() -def _account(account_id: str) -> Account: + +def _account( + account_id: str, + *, + status: AccountStatus = AccountStatus.ACTIVE, + chatgpt_account_id: str | None | object = _DEFAULT_CHATGPT_ACCOUNT_ID, +) -> Account: + if chatgpt_account_id is _DEFAULT_CHATGPT_ACCOUNT_ID: + resolved_chatgpt_account_id: str | None = f"workspace-{account_id}" + else: + resolved_chatgpt_account_id = cast("str | None", chatgpt_account_id) return Account( id=account_id, - chatgpt_account_id=f"workspace-{account_id}", + chatgpt_account_id=resolved_chatgpt_account_id, email=f"{account_id}@example.com", plan_type="plus", access_token_encrypted=b"", refresh_token_encrypted=b"", id_token_encrypted=b"", last_refresh=datetime(2025, 1, 1), - status=AccountStatus.ACTIVE, + status=status, ) @@ -64,6 +76,35 @@ def test_account_summary_returns_zero_and_null_when_no_snapshot() -> None: assert summary.reset_credit_nearest_expires_at is None +@pytest.mark.parametrize("status", [AccountStatus.PAUSED, AccountStatus.DEACTIVATED]) +def test_account_summary_suppresses_cached_reset_credits_for_ineligible_status(status: AccountStatus) -> None: + store = RateLimitResetCreditsStore() + store._snapshots["acc_ineligible"] = RateLimitResetCreditsSnapshot( # type: ignore[attr-defined] + available_count=3, + nearest_expires_at=datetime(2026, 6, 20, 0, 0, 0), + credits=[], + ) + + [summary] = _summaries([_account("acc_ineligible", status=status)], store) + + assert summary.available_reset_credits == 0 + assert summary.reset_credit_nearest_expires_at is None + + +def test_account_summary_suppresses_cached_reset_credits_without_chatgpt_account_id() -> None: + store = RateLimitResetCreditsStore() + store._snapshots["acc_no_workspace"] = RateLimitResetCreditsSnapshot( # type: ignore[attr-defined] + available_count=3, + nearest_expires_at=datetime(2026, 6, 20, 0, 0, 0), + credits=[], + ) + + [summary] = _summaries([_account("acc_no_workspace", chatgpt_account_id=None)], store) + + assert summary.available_reset_credits == 0 + assert summary.reset_credit_nearest_expires_at is None + + def test_account_summary_mixed_cache_state_across_accounts() -> None: store = RateLimitResetCreditsStore() store._snapshots["acc_has"] = RateLimitResetCreditsSnapshot( # type: ignore[attr-defined] diff --git a/tests/unit/test_rate_limit_reset_credits_scheduler.py b/tests/unit/test_rate_limit_reset_credits_scheduler.py index c9371a26e..cb3867810 100644 --- a/tests/unit/test_rate_limit_reset_credits_scheduler.py +++ b/tests/unit/test_rate_limit_reset_credits_scheduler.py @@ -92,6 +92,32 @@ async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> assert store.get("acc_active") is not None +@pytest.mark.asyncio +async def test_refresh_invalidates_snapshots_for_paused_and_deactivated_accounts() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_paused", RateLimitResetCreditsSnapshot(available_count=1)) + await store.set("acc_deactivated", RateLimitResetCreditsSnapshot(available_count=1)) + fetched: list[str] = [] + + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: + fetched.append(access_token) + return _response() + + await refresh_reset_credits_for_accounts( + accounts=[ + _make_account("acc_paused", status=AccountStatus.PAUSED), + _make_account("acc_deactivated", status=AccountStatus.DEACTIVATED), + ], + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + assert fetched == [] + assert store.get("acc_paused") is None + assert store.get("acc_deactivated") is None + + @pytest.mark.asyncio async def test_refresh_skips_account_without_chatgpt_account_id() -> None: store = RateLimitResetCreditsStore() @@ -112,6 +138,24 @@ async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> assert store.get("acc_no_workspace") is None +@pytest.mark.asyncio +async def test_refresh_invalidates_snapshot_for_account_without_chatgpt_account_id() -> None: + store = RateLimitResetCreditsStore() + await store.set("acc_no_workspace", RateLimitResetCreditsSnapshot(available_count=1)) + + async def fetch_fn(access_token: str, account_id: str | None, **kwargs: Any) -> ResetCreditsResponse: + raise AssertionError("ineligible account must not fetch reset credits") + + await refresh_reset_credits_for_accounts( + accounts=[_make_account("acc_no_workspace", chatgpt_account_id=None)], + encryptor=StubEncryptor(), + store=store, + fetch_fn=fetch_fn, + ) + + assert store.get("acc_no_workspace") is None + + @pytest.mark.asyncio async def test_one_account_failure_does_not_break_the_loop() -> None: store = RateLimitResetCreditsStore() From 166572cb067fafdf9024e13b11662ee96ff5dbfc Mon Sep 17 00:00:00 2001 From: ellentane Date: Sun, 21 Jun 2026 15:50:42 +0200 Subject: [PATCH 10/39] fix(reset-credits): critical consume hardening and zero-db migration adaptation --- app/core/clients/rate_limit_reset_credits.py | 316 +- app/core/usage/models.py | 7 + .../usage/reset_credits_refresh_scheduler.py | 123 +- app/modules/accounts/schemas.py | 6 +- app/modules/rate_limit_reset_credits/api.py | 317 +- frontend/package-lock.json | 14983 ++++++++++++++++ .../src/components/layout/app-header.test.tsx | 19 + .../components/account-actions.test.tsx | 35 + .../accounts/components/account-actions.tsx | 7 +- .../accounts/components/accounts-page.tsx | 14 +- .../reset-credit-confirm-dialog.test.tsx | 105 +- .../reset-credit-confirm-dialog.tsx | 79 +- .../features/accounts/hooks/use-accounts.ts | 9 +- .../src/features/accounts/schemas.test.ts | 37 +- frontend/src/features/accounts/schemas.ts | 6 +- .../components/account-card.test.tsx | 23 +- .../dashboard/components/account-card.tsx | 14 +- .../components/account-list.test.tsx | 11 +- .../dashboard/components/account-list.tsx | 15 +- .../dashboard/components/dashboard-page.tsx | 11 +- .../add-rate-limit-reset-credits/design.md | 20 +- .../specs/frontend-architecture/spec.md | 8 +- .../specs/rate-limit-reset-credits/context.md | 66 +- .../specs/rate-limit-reset-credits/spec.md | 54 +- .../add-rate-limit-reset-credits/tasks.md | 12 +- .../test_rate_limit_reset_credits_api.py | 197 + .../unit/test_rate_limit_reset_credits_api.py | 325 +- .../test_rate_limit_reset_credits_client.py | 75 - ...test_rate_limit_reset_credits_scheduler.py | 69 +- 29 files changed, 16251 insertions(+), 712 deletions(-) create mode 100644 frontend/package-lock.json create mode 100644 tests/integration/test_rate_limit_reset_credits_api.py diff --git a/app/core/clients/rate_limit_reset_credits.py b/app/core/clients/rate_limit_reset_credits.py index 9c922d6ca..2638ee424 100644 --- a/app/core/clients/rate_limit_reset_credits.py +++ b/app/core/clients/rate_limit_reset_credits.py @@ -17,6 +17,10 @@ ) from app.core.clients.headers import build_chatgpt_auth_headers from app.core.clients.http import lease_retry_client +from app.core.clients.usage import ( + _retry_delay_seconds, + _safe_codex_json, +) from app.core.config.settings import get_settings from app.core.types import JsonObject from app.core.upstream_proxy import ResolvedUpstreamRoute @@ -125,7 +129,7 @@ async def fetch_reset_credits( try: if route is not None: - return await _fetch_reset_credits_via_codex( + data = await _fetch_reset_credits_via_codex( url=url, route=route, headers=headers, @@ -133,31 +137,32 @@ async def fetch_reset_credits( retries=retries, codex_client=codex_client, ) - async with lease_retry_client(client) as retry_client: - async with retry_client.request( - "GET", - url, - headers=headers, - timeout=timeout, - retry_options=retry_options, - ) as resp: - data = await _safe_json(resp) - if resp.status >= 400: - code = _extract_error_code(data) - message = _extract_error_message(data) or f"Reset credits fetch failed ({resp.status})" - logger.warning( - "Reset credits fetch failed request_id=%s status=%s code=%s message=%s", - get_request_id(), - resp.status, - code, - message, - ) - raise ResetCreditFetchError(resp.status, message, code=code) - try: - return ResetCreditsResponse.model_validate(_success_payload(data)) - except (ValueError, ValidationError) as exc: - logger.warning("Reset credits fetch invalid payload request_id=%s", get_request_id()) - raise ResetCreditFetchError(502, "Invalid reset credits payload") from exc + else: + async with lease_retry_client(client) as retry_client: + async with retry_client.request( + "GET", + url, + headers=headers, + timeout=timeout, + retry_options=retry_options, + ) as resp: + data = await _safe_json(resp) + if resp.status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits fetch failed ({resp.status})" + logger.warning( + "Reset credits fetch failed request_id=%s status=%s code=%s message=%s", + get_request_id(), + resp.status, + code, + message, + ) + raise ResetCreditFetchError(resp.status, message, code=code) + try: + return ResetCreditsResponse.model_validate(_success_payload(data)) + except (ValueError, ValidationError) as exc: + logger.warning("Reset credits fetch invalid payload request_id=%s", get_request_id()) + raise ResetCreditFetchError(502, "Invalid reset credits payload") from exc except (aiohttp.ClientError, asyncio.TimeoutError, CodexTransportError) as exc: logger.warning("Reset credits fetch error request_id=%s error=%s", get_request_id(), exc) raise ResetCreditFetchError(0, f"Reset credits fetch failed: {exc}") from exc @@ -180,7 +185,7 @@ async def consume_reset_credit( usage_base = base_url or settings.upstream_base_url url = _consume_url(usage_base) timeout = aiohttp.ClientTimeout(total=timeout_seconds or settings.usage_fetch_timeout_seconds) - retries = max_retries if max_retries is not None else 0 + retries = max_retries if max_retries is not None else settings.usage_fetch_max_retries headers = build_chatgpt_auth_headers( access_token, account_id, @@ -237,123 +242,6 @@ async def consume_reset_credit( raise ConsumeResetCreditError(0, f"Reset credits consume failed: {exc}") from exc -async def _fetch_reset_credits_via_codex( - *, - url: str, - route: ResolvedUpstreamRoute, - headers: dict[str, str], - timeout_seconds: float, - retries: int, - codex_client: CodexClient | None, -) -> ResetCreditsResponse: - attempts = max(1, retries + 1) - owns_codex_client = codex_client is None - active_codex_client = codex_client or CodexClient(create_codex_session()) - try: - for attempt in range(attempts): - try: - data, status = await _request_json_via_codex( - active_codex_client, - "GET", - url, - route=route, - headers=headers, - timeout_seconds=timeout_seconds, - ) - except CodexTransportError: - if attempt < attempts - 1: - await asyncio.sleep(_retry_delay_seconds(attempt)) - continue - raise - if status in RETRYABLE_STATUS and attempt < attempts - 1: - await asyncio.sleep(_retry_delay_seconds(attempt)) - continue - if status >= 400: - code = _extract_error_code(data) - message = _extract_error_message(data) or f"Reset credits fetch failed ({status})" - raise ResetCreditFetchError(status, message, code=code) - try: - return ResetCreditsResponse.model_validate(_success_payload(data)) - except (ValueError, ValidationError) as exc: - raise ResetCreditFetchError(502, "Invalid reset credits payload") from exc - finally: - if owns_codex_client: - close = getattr(active_codex_client, "close", None) - if callable(close): - await close() - raise RuntimeError("unreachable reset credits fetch retry state") - - -async def _consume_reset_credit_via_codex( - *, - url: str, - route: ResolvedUpstreamRoute, - headers: dict[str, str], - body: JsonObject, - timeout_seconds: float, - retries: int, - codex_client: CodexClient | None, -) -> ConsumeResetCreditResponse: - attempts = max(1, retries + 1) - owns_codex_client = codex_client is None - active_codex_client = codex_client or CodexClient(create_codex_session()) - try: - for attempt in range(attempts): - try: - data, status = await _request_json_via_codex( - active_codex_client, - "POST", - url, - route=route, - headers=headers, - json_body=body, - timeout_seconds=timeout_seconds, - ) - except CodexTransportError: - if attempt < attempts - 1: - await asyncio.sleep(_retry_delay_seconds(attempt)) - continue - raise - if status in RETRYABLE_STATUS and attempt < attempts - 1: - await asyncio.sleep(_retry_delay_seconds(attempt)) - continue - if status >= 400: - code = _extract_error_code(data) - message = _extract_error_message(data) or f"Reset credits consume failed ({status})" - raise ConsumeResetCreditError(status, message, code=code) - try: - return ConsumeResetCreditResponse.model_validate(_success_payload(data)) - except (ValueError, ValidationError) as exc: - raise ConsumeResetCreditError(502, "Invalid reset credits consume payload") from exc - finally: - if owns_codex_client: - close = getattr(active_codex_client, "close", None) - if callable(close): - await close() - raise RuntimeError("unreachable reset credits consume retry state") - - -async def _request_json_via_codex( - codex_client: CodexClient, - method: str, - url: str, - *, - route: ResolvedUpstreamRoute, - headers: dict[str, str], - timeout_seconds: float, - json_body: JsonObject | None = None, -) -> tuple[JsonObject, int]: - request_kwargs: dict[str, object] = { - "route": route, - "headers": headers, - "timeout": timeout_seconds, - } - if json_body is not None: - request_kwargs["json"] = json_body - resp = await codex_client.request(method, url, **request_kwargs) - return await _safe_codex_json(resp), _codex_response_status(resp) - - def build_snapshot(response: ResetCreditsResponse) -> RateLimitResetCreditsSnapshot: """Project an upstream list response into the cached snapshot shape.""" nearest = _nearest_available_expires_at(response.credits) @@ -382,41 +270,6 @@ def _consume_url(base_url: str) -> str: return f"{_reset_credits_url(base_url)}/consume" -def _codex_response_status(response: object) -> int: - value = getattr(response, "status_code", getattr(response, "status", None)) - if value is None: - return 0 - return int(value) - - -async def _safe_codex_json(response: object) -> JsonObject: - try: - json_method = getattr(response, "json", None) - if callable(json_method): - data = json_method() - if asyncio.iscoroutine(data): - data = await data - return data if isinstance(data, dict) else {"error": {"message": str(data)}} - except Exception: - pass - content = getattr(response, "content", None) - if isinstance(content, bytes): - return {"error": {"message": content.decode("utf-8", errors="replace").strip()}} - if isinstance(content, str): - return {"error": {"message": content.strip()}} - text_method = getattr(response, "text", None) - if callable(text_method): - try: - text = text_method() - if asyncio.iscoroutine(text): - text = await text - if isinstance(text, str): - return {"error": {"message": text.strip()}} - except Exception: - pass - return {"error": {"message": ""}} - - async def _safe_json(resp: aiohttp.ClientResponse) -> JsonObject: try: data = await resp.json(content_type=None) @@ -478,5 +331,106 @@ def _retry_options(attempts: int) -> ExponentialRetry: ) -def _retry_delay_seconds(attempt: int) -> float: - return min(RETRY_MAX_TIMEOUT, RETRY_START_TIMEOUT * (2.0**attempt)) +async def _fetch_reset_credits_via_codex( + *, + url: str, + route: ResolvedUpstreamRoute, + headers: dict[str, str], + timeout_seconds: float, + retries: int, + codex_client: CodexClient | None, +) -> JsonObject: + attempts = max(1, retries + 1) + owns_codex_client = codex_client is None + active_codex_client = codex_client or CodexClient(create_codex_session()) + try: + for attempt in range(attempts): + try: + resp = await active_codex_client.request( + "GET", + url, + route=route, + headers=headers, + timeout=timeout_seconds, + ) + except CodexTransportError: + if attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + raise + + data = await _safe_codex_json(resp) + status = _codex_response_status(resp) + if status in RETRYABLE_STATUS and attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + if status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits fetch failed ({status})" + raise ResetCreditFetchError(status, message, code=code) + return data if isinstance(data, dict) else {"error": {"message": str(data)}} + finally: + if owns_codex_client: + close = getattr(active_codex_client, "close", None) + if callable(close): + await close() + raise RuntimeError("unreachable reset credits fetch retry state") + + +async def _consume_reset_credit_via_codex( + *, + url: str, + route: ResolvedUpstreamRoute, + headers: dict[str, str], + body: dict[str, str], + timeout_seconds: float, + retries: int, + codex_client: CodexClient | None, +) -> ConsumeResetCreditResponse: + attempts = max(1, retries + 1) + owns_codex_client = codex_client is None + active_codex_client = codex_client or CodexClient(create_codex_session()) + try: + for attempt in range(attempts): + try: + resp = await active_codex_client.request( + "POST", + url, + route=route, + headers=headers, + json=body, + timeout=timeout_seconds, + ) + except CodexTransportError: + if attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + raise + + data = await _safe_codex_json(resp) + status = _codex_response_status(resp) + if status in RETRYABLE_STATUS and attempt < attempts - 1: + await asyncio.sleep(_retry_delay_seconds(attempt)) + continue + if status >= 400: + code = _extract_error_code(data) + message = _extract_error_message(data) or f"Reset credits consume failed ({status})" + raise ConsumeResetCreditError(status, message, code=code) + try: + return ConsumeResetCreditResponse.model_validate(_success_payload(data)) + except (ValueError, ValidationError) as exc: + logger.warning("Reset credits consume invalid payload request_id=%s", get_request_id()) + raise ConsumeResetCreditError(502, "Invalid reset credits consume payload") from exc + finally: + if owns_codex_client: + close = getattr(active_codex_client, "close", None) + if callable(close): + await close() + raise RuntimeError("unreachable reset credits consume retry state") + + +def _codex_response_status(response: object) -> int: + value = getattr(response, "status_code", getattr(response, "status", None)) + if value is None: + return 0 + return int(value) diff --git a/app/core/usage/models.py b/app/core/usage/models.py index f37c08f1e..1a531e517 100644 --- a/app/core/usage/models.py +++ b/app/core/usage/models.py @@ -27,6 +27,12 @@ class CreditsPayload(BaseModel): balance: str | None = None +class RateLimitResetCreditsSummary(BaseModel): + model_config = ConfigDict(extra="ignore") + + available_count: int | None = None + + class AdditionalRateLimitPayload(BaseModel): model_config = ConfigDict(extra="ignore") @@ -44,4 +50,5 @@ class UsagePayload(BaseModel): seat_type: str | None = None rate_limit: RateLimitPayload | None = None credits: CreditsPayload | None = None + rate_limit_reset_credits: RateLimitResetCreditsSummary | None = None additional_rate_limits: list[AdditionalRateLimitPayload] | None = None diff --git a/app/core/usage/reset_credits_refresh_scheduler.py b/app/core/usage/reset_credits_refresh_scheduler.py index 093f985a4..d93110139 100644 --- a/app/core/usage/reset_credits_refresh_scheduler.py +++ b/app/core/usage/reset_credits_refresh_scheduler.py @@ -3,31 +3,38 @@ import asyncio import contextlib import logging -from collections.abc import Awaitable, Callable +from collections.abc import AsyncIterator, Awaitable, Callable +from contextlib import asynccontextmanager from dataclasses import dataclass, field +from app.core.auth.refresh import RefreshError from app.core.clients.rate_limit_reset_credits import ( + ResetCreditFetchError, ResetCreditsResponse, build_snapshot, fetch_reset_credits, ) from app.core.config.settings import get_settings from app.core.crypto import TokenEncryptor -from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route +from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError from app.db.models import Account, AccountStatus from app.db.session import get_background_session +from app.modules.accounts.auth_manager import AuthManager from app.modules.accounts.repository import AccountsRepository from app.modules.rate_limit_reset_credits.store import ( RateLimitResetCreditsStore, get_rate_limit_reset_credits_store, ) +from app.modules.usage.updater import _resolve_upstream_route_for_account logger = logging.getLogger(__name__) -_RESET_CREDITS_SKIP_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.DEACTIVATED}) +_RESET_CREDITS_SKIP_STATUSES = frozenset( + {AccountStatus.PAUSED, AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED} +) ResetCreditsFetchFn = Callable[..., Awaitable[ResetCreditsResponse]] -UpstreamRouteResolver = Callable[[Account], Awaitable[ResolvedUpstreamRoute | None]] +ResolveRouteFn = Callable[[Account], Awaitable[ResolvedUpstreamRoute | None]] @dataclass(slots=True) @@ -66,12 +73,17 @@ async def _refresh_once(self) -> None: async with get_background_session() as session: accounts_repo = AccountsRepository(session) accounts = await accounts_repo.list_accounts() + auth_manager = AuthManager( + accounts_repo, + refresh_repo_factory=_reset_credits_accounts_repo_factory, + ) await refresh_reset_credits_for_accounts( accounts=accounts, encryptor=TokenEncryptor(), store=get_rate_limit_reset_credits_store(), fetch_fn=fetch_reset_credits, - route_resolver=_resolve_upstream_route_for_account, + resolve_route=_resolve_reset_credits_refresh_route, + auth_manager=auth_manager, ) except Exception: logger.exception("Reset credits refresh loop failed") @@ -83,7 +95,8 @@ async def refresh_reset_credits_for_accounts( encryptor: TokenEncryptor, store: RateLimitResetCreditsStore, fetch_fn: ResetCreditsFetchFn = fetch_reset_credits, - route_resolver: UpstreamRouteResolver | None = None, + resolve_route: ResolveRouteFn | None = None, + auth_manager: AuthManager | None = None, ) -> None: """Refresh the cached reset-credits snapshot for each eligible account. @@ -93,20 +106,22 @@ async def refresh_reset_credits_for_accounts( stays owned by usage refresh. One account failing must not abort the loop. """ for account in accounts: - if not _is_reset_credits_refresh_eligible(account): - await store.invalidate(account.id) + if account.status in _RESET_CREDITS_SKIP_STATUSES: + continue + if not account.chatgpt_account_id: continue await _refresh_account_reset_credits( account, encryptor=encryptor, store=store, fetch_fn=fetch_fn, - route_resolver=route_resolver, + resolve_route=resolve_route, + auth_manager=auth_manager, ) -def _is_reset_credits_refresh_eligible(account: Account) -> bool: - return account.status not in _RESET_CREDITS_SKIP_STATUSES and bool(account.chatgpt_account_id) +async def _resolve_reset_credits_refresh_route(account: Account) -> ResolvedUpstreamRoute | None: + return await _resolve_upstream_route_for_account(account, operation="usage_refresh") async def _refresh_account_reset_credits( @@ -115,32 +130,62 @@ async def _refresh_account_reset_credits( encryptor: TokenEncryptor, store: RateLimitResetCreditsStore, fetch_fn: ResetCreditsFetchFn, - route_resolver: UpstreamRouteResolver | None, + resolve_route: ResolveRouteFn | None = None, + auth_manager: AuthManager | None = None, ) -> None: snapshot_generation = store.generation(account.id) - try: - access_token = encryptor.decrypt(account.access_token_encrypted) - route = await route_resolver(account) if route_resolver is not None else None - response = await fetch_fn( - access_token, - account.chatgpt_account_id, - route=route, - allow_direct_egress=route is None, - ) - except UpstreamProxyRouteError as exc: - logger.warning( - "Reset credits route resolution failed account_id=%s reason=%s", - account.id, - exc.reason, - ) - return - except Exception as exc: # scheduler must never crash the loop or mutate account status - logger.warning( - "Reset credits refresh failed account_id=%s error=%s", - account.id, - exc, - ) + refresh_account = account + response: ResetCreditsResponse | None = None + + for attempt in range(2): + route: ResolvedUpstreamRoute | None = None + if resolve_route is not None: + try: + route = await resolve_route(refresh_account) + except UpstreamProxyRouteError as exc: + logger.warning( + "Reset credits refresh upstream proxy route unavailable account_id=%s reason=%s", + refresh_account.id, + exc.reason, + ) + return + try: + access_token = encryptor.decrypt(refresh_account.access_token_encrypted) + response = await fetch_fn( + access_token, + refresh_account.chatgpt_account_id, + route=route, + allow_direct_egress=route is None, + ) + break + except ResetCreditFetchError as exc: + if exc.status_code != 401 or auth_manager is None or attempt > 0: + logger.warning( + "Reset credits refresh failed account_id=%s error=%s", + refresh_account.id, + exc, + ) + return + try: + refresh_account = await auth_manager.ensure_fresh(refresh_account, force=True) + except RefreshError as refresh_exc: + logger.warning( + "Reset credits refresh token refresh failed account_id=%s error=%s", + refresh_account.id, + refresh_exc, + ) + return + except Exception as exc: # scheduler must never crash the loop or mutate account status + logger.warning( + "Reset credits refresh failed account_id=%s error=%s", + refresh_account.id, + exc, + ) + return + + if response is None: return + snapshot = build_snapshot(response) stored = await store.set_if_generation(account.id, snapshot, snapshot_generation) if not stored: @@ -150,14 +195,10 @@ async def _refresh_account_reset_credits( ) -async def _resolve_upstream_route_for_account(account: Account) -> ResolvedUpstreamRoute | None: +@asynccontextmanager +async def _reset_credits_accounts_repo_factory() -> AsyncIterator[AccountsRepository]: async with get_background_session() as session: - return await resolve_upstream_route( - session, - account_id=account.id, - operation="reset_credits_refresh", - scope="account", - ) + yield AccountsRepository(session) def build_rate_limit_reset_credits_scheduler() -> RateLimitResetCreditsRefreshScheduler: diff --git a/app/modules/accounts/schemas.py b/app/modules/accounts/schemas.py index 93ba9da3b..68bd4c3c5 100644 --- a/app/modules/accounts/schemas.py +++ b/app/modules/accounts/schemas.py @@ -115,10 +115,8 @@ class AccountSummary(DashboardModel): # surface a "delete older" action without requiring the operator to # group rows by email themselves. See codex-lb #787 (B). is_email_duplicate: bool = False - # Banked rate-limit reset credits joined from the in-memory snapshot - # (refreshed by each replica's reset-credits scheduler). ``0`` / ``null`` - # when no snapshot is cached yet (e.g. right after restart); the dashboard - # hides all reset affordances in that case. + # Banked rate-limit reset credits from the in-memory snapshot when cached, + # otherwise the latest persisted primary usage_history count from /wham/usage. available_reset_credits: int = 0 reset_credit_nearest_expires_at: datetime | None = None diff --git a/app/modules/rate_limit_reset_credits/api.py b/app/modules/rate_limit_reset_credits/api.py index 3adffb1b7..48a2c6a54 100644 --- a/app/modules/rate_limit_reset_credits/api.py +++ b/app/modules/rate_limit_reset_credits/api.py @@ -1,23 +1,31 @@ from __future__ import annotations import asyncio +import logging +from collections.abc import Awaitable, Callable +from dataclasses import dataclass from datetime import datetime, timezone -from typing import Awaitable, Callable -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request from pydantic import Field +from app.core.audit.service import AuditService from app.core.auth.dependencies import ( require_dashboard_write_access, set_dashboard_error_format, validate_dashboard_session, ) +from app.core.auth.refresh import RefreshError from app.core.clients.rate_limit_reset_credits import ( ConsumeResetCreditError, ConsumeResetCreditResponse, RateLimitResetCreditsSnapshot, + ResetCreditFetchError, ResetCreditItem, + ResetCreditsResponse, + build_snapshot, consume_reset_credit, + fetch_reset_credits, ) from app.core.crypto import TokenEncryptor from app.core.exceptions import ( @@ -27,14 +35,20 @@ DashboardPermissionError, DashboardServiceUnavailableError, ) -from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route +from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError +from app.core.usage.reset_credits_refresh_scheduler import _refresh_account_reset_credits from app.db.models import Account, AccountStatus from app.dependencies import AccountsContext, get_accounts_context +from app.modules.accounts.auth_manager import AuthManager +from app.modules.proxy.account_cache import get_account_selection_cache from app.modules.rate_limit_reset_credits.store import ( RateLimitResetCreditsStore, get_rate_limit_reset_credits_store, ) from app.modules.shared.schemas import DashboardModel +from app.modules.usage.updater import _resolve_upstream_route_for_account + +logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/accounts", @@ -42,10 +56,17 @@ dependencies=[Depends(validate_dashboard_session), Depends(set_dashboard_error_format)], ) +FetchFn = Callable[..., Awaitable[ResetCreditsResponse]] ConsumeFn = Callable[..., Awaitable[ConsumeResetCreditResponse]] +RefreshUsageFn = Callable[[Account], Awaitable[None]] +ResolveRouteFn = Callable[[Account], Awaitable[ResolvedUpstreamRoute | None]] + +_NON_REDEEMABLE_STATUSES = frozenset( + {AccountStatus.PAUSED, AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED} +) + _redeem_locks: dict[str, asyncio.Lock] = {} _redeem_locks_registry_lock = asyncio.Lock() -_RESET_CREDITS_INELIGIBLE_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.DEACTIVATED}) class ResetCreditItemResponse(DashboardModel): @@ -72,15 +93,40 @@ class ConsumeResetCreditResponseSchema(DashboardModel): redeemed_at: datetime | None = None +@dataclass(frozen=True, slots=True) +class _RedeemResetCreditOutcome: + response: ConsumeResetCreditResponseSchema + available_count_after: int + + @router.get( "/{account_id}/rate-limit-reset-credits", response_model=RateLimitResetCreditsSnapshotResponse | None, ) async def get_rate_limit_reset_credits( account_id: str, + context: AccountsContext = Depends(get_accounts_context), ) -> RateLimitResetCreditsSnapshotResponse | None: - snapshot = get_rate_limit_reset_credits_store().get(account_id) - return _snapshot_to_response(snapshot) + store = get_rate_limit_reset_credits_store() + snapshot = store.get(account_id) + if snapshot is not None: + return _snapshot_to_response(snapshot) + + account = await context.repository.get_by_id(account_id) + if account is None: + return None + if account.status in _NON_REDEEMABLE_STATUSES or not account.chatgpt_account_id: + return None + + await _refresh_account_reset_credits( + account, + encryptor=TokenEncryptor(), + store=store, + fetch_fn=fetch_reset_credits, + auth_manager=context.service._auth_manager, + resolve_route=_resolve_reset_credit_route, + ) + return _snapshot_to_response(store.get(account_id)) @router.post( @@ -88,6 +134,7 @@ async def get_rate_limit_reset_credits( response_model=ConsumeResetCreditResponseSchema, ) async def consume_rate_limit_reset_credit( + request: Request, account_id: str, _write_access=Depends(require_dashboard_write_access), context: AccountsContext = Depends(get_accounts_context), @@ -95,32 +142,43 @@ async def consume_rate_limit_reset_credit( account = await context.repository.get_by_id(account_id) if account is None: raise DashboardNotFoundError("Account not found", code="account_not_found") + store = get_rate_limit_reset_credits_store() - if not _account_can_redeem_reset_credit(account): - await store.invalidate(account.id) - raise DashboardConflictError( - "Account is not eligible to redeem reset credits", - code="reset_credit_account_ineligible", - ) + cached_snapshot = store.get(account_id) + available_count_before = cached_snapshot.available_count if cached_snapshot is not None else 0 + try: - route = await resolve_upstream_route( - context.session, - account_id=account.id, - operation="reset_credits_consume", - scope="account", + outcome = await _redeem_soonest_reset_credit( + account=account, + store=store, + encryptor=TokenEncryptor(), + auth_manager=context.service._auth_manager, + refresh_usage=_build_refresh_usage_callback(context), + resolve_route=_resolve_reset_credit_route, ) + except RefreshError as exc: + raise DashboardConflictError( + f"Reset credit consume could not refresh account credentials: {exc.message}", + code="account_reset_credit_refresh_failed", + ) from exc except UpstreamProxyRouteError as exc: - raise DashboardServiceUnavailableError( - "Unable to resolve upstream proxy route for reset-credit consume", - code=exc.reason, + raise DashboardConflictError( + f"Reset credit consume upstream proxy route unavailable: {exc.reason}", + code="account_reset_credit_upstream_route_unavailable", ) from exc - return await _redeem_soonest_reset_credit( - account=account, - store=store, - encryptor=TokenEncryptor(), - consume_fn=consume_reset_credit, - route=route, + + AuditService.log_async( + "account_rate_limit_reset_credit_consumed", + actor_ip=request.client.host if request.client else None, + details={ + "account_id": account_id, + "consume_code": outcome.response.code, + "windows_reset": outcome.response.windows_reset, + "available_reset_credits_before": available_count_before, + "available_reset_credits_after": outcome.available_count_after, + }, ) + return outcome.response async def _redeem_soonest_reset_credit( @@ -128,37 +186,171 @@ async def _redeem_soonest_reset_credit( account: Account, store: RateLimitResetCreditsStore, encryptor: TokenEncryptor, - consume_fn: ConsumeFn, - route: ResolvedUpstreamRoute | None = None, -) -> ConsumeResetCreditResponseSchema: + fetch_fn: FetchFn | None = None, + consume_fn: ConsumeFn | None = None, + auth_manager: AuthManager | None = None, + refresh_usage: RefreshUsageFn | None = None, + resolve_route: ResolveRouteFn | None = None, +) -> _RedeemResetCreditOutcome: + _assert_account_can_redeem_reset_credit(account) + effective_fetch_fn = fetch_fn or fetch_reset_credits + effective_consume_fn = consume_fn or consume_reset_credit + lock = await get_reset_credit_redeem_lock(account.id) - async with lock: - snapshot = store.get(account.id) - credit = _select_soonest_available_credit(snapshot) - if credit is None: - raise DashboardConflictError("No available reset credit", code="no_available_reset_credit") - access_token = encryptor.decrypt(account.access_token_encrypted) + try: + async with lock: + return await _redeem_soonest_reset_credit_locked( + account=account, + store=store, + encryptor=encryptor, + effective_fetch_fn=effective_fetch_fn, + effective_consume_fn=effective_consume_fn, + auth_manager=auth_manager, + refresh_usage=refresh_usage, + resolve_route=resolve_route, + ) + finally: + await _prune_redeem_lock_if_idle(account.id, lock) + + +async def _redeem_soonest_reset_credit_locked( + *, + account: Account, + store: RateLimitResetCreditsStore, + encryptor: TokenEncryptor, + effective_fetch_fn: FetchFn, + effective_consume_fn: ConsumeFn, + auth_manager: AuthManager | None, + refresh_usage: RefreshUsageFn | None, + resolve_route: ResolveRouteFn | None, +) -> _RedeemResetCreditOutcome: + redeem_account = account + if auth_manager is not None: + redeem_account = await auth_manager.ensure_fresh(account, force=False) + + access_token = encryptor.decrypt(redeem_account.access_token_encrypted) + route: ResolvedUpstreamRoute | None = None + if resolve_route is not None: + route = await resolve_route(redeem_account) + + try: + credits_response = await effective_fetch_fn( + access_token, + redeem_account.chatgpt_account_id, + route=route, + allow_direct_egress=route is None, + ) + except ResetCreditFetchError as exc: + raise _translate_fetch_error(exc) from exc + + credit = _select_soonest_available_credit_from_response(credits_response) + if credit is None: + raise DashboardConflictError("No available reset credit", code="no_available_reset_credit") + + try: + result = await effective_consume_fn( + access_token, + redeem_account.chatgpt_account_id, + credit.id, + route=route, + allow_direct_egress=route is None, + ) + except ConsumeResetCreditError as exc: + raise _translate_consume_error(exc) from exc + + redeemed_at = result.credit.redeemed_at if result.credit else None + available_count_after = max(0, credits_response.available_count - 1) + await store.invalidate(account.id) + + if refresh_usage is not None: try: - result = await consume_fn( - access_token, - account.chatgpt_account_id, - credit.id, - route=route, - allow_direct_egress=route is None, + await refresh_usage(redeem_account) + except Exception: + logger.warning( + "Reset credit consume succeeded but usage refresh failed account_id=%s", + account.id, + exc_info=True, + ) + await _try_restore_reset_credits_snapshot_after_consume( + account=account, + redeem_account=redeem_account, + encryptor=encryptor, + store=store, + fetch_fn=effective_fetch_fn, + resolve_route=resolve_route, ) - except ConsumeResetCreditError as exc: - raise _translate_consume_error(exc) from exc - redeemed_at = result.credit.redeemed_at if result.credit else None - await store.invalidate(account.id) - return ConsumeResetCreditResponseSchema( + + return _RedeemResetCreditOutcome( + response=ConsumeResetCreditResponseSchema( code=result.code, windows_reset=result.windows_reset, redeemed_at=redeemed_at, + ), + available_count_after=available_count_after, + ) + + +async def _prune_redeem_lock_if_idle(account_id: str, lock: asyncio.Lock) -> None: + if lock.locked(): + return + async with _redeem_locks_registry_lock: + if _redeem_locks.get(account_id) is lock and not lock.locked(): + _redeem_locks.pop(account_id, None) + + +def _assert_account_can_redeem_reset_credit(account: Account) -> None: + if account.status in _NON_REDEEMABLE_STATUSES: + raise DashboardConflictError( + f"Account is {account.status.value} and cannot redeem a reset credit", + code="account_not_reset_credit_applicable", ) -def _account_can_redeem_reset_credit(account: Account) -> bool: - return account.status not in _RESET_CREDITS_INELIGIBLE_STATUSES and bool(account.chatgpt_account_id) +def _build_refresh_usage_callback(context: AccountsContext) -> RefreshUsageFn | None: + usage_updater = context.service._usage_updater + if usage_updater is None: + return None + + async def refresh_usage(account: Account) -> None: + await usage_updater.force_refresh(account) + get_account_selection_cache().invalidate() + + return refresh_usage + + +async def _resolve_reset_credit_route(account: Account) -> ResolvedUpstreamRoute | None: + return await _resolve_upstream_route_for_account(account, operation="rate_limit_reset_consume") + + +async def _try_restore_reset_credits_snapshot_after_consume( + *, + account: Account, + redeem_account: Account, + encryptor: TokenEncryptor, + store: RateLimitResetCreditsStore, + fetch_fn: FetchFn, + resolve_route: ResolveRouteFn | None, +) -> None: + """Best-effort cache repopulation when usage refresh fails after a successful consume.""" + try: + access_token = encryptor.decrypt(redeem_account.access_token_encrypted) + route: ResolvedUpstreamRoute | None = None + if resolve_route is not None: + route = await resolve_route(redeem_account) + credits_response = await fetch_fn( + access_token, + redeem_account.chatgpt_account_id, + route=route, + allow_direct_egress=route is None, + ) + except Exception: + logger.warning( + "Reset credit consume post-refresh re-fetch failed account_id=%s", + account.id, + exc_info=True, + ) + return + await store.set(account.id, build_snapshot(credits_response)) async def get_reset_credit_redeem_lock(account_id: str) -> asyncio.Lock: @@ -173,6 +365,16 @@ async def get_reset_credit_redeem_lock(account_id: str) -> asyncio.Lock: return lock +def _translate_fetch_error(exc: ResetCreditFetchError) -> Exception: + if exc.status_code == 401: + return DashboardAuthError(exc.message, code=exc.code) + if exc.status_code == 403: + return DashboardPermissionError(exc.message, code=exc.code) + if exc.status_code == 409: + return DashboardConflictError(exc.message, code=exc.code) + return DashboardServiceUnavailableError(exc.message, code=exc.code) + + def _translate_consume_error(exc: ConsumeResetCreditError) -> Exception: if exc.status_code == 401: return DashboardAuthError(exc.message, code=exc.code) @@ -188,9 +390,22 @@ def _select_soonest_available_credit( ) -> ResetCreditItem | None: if snapshot is None: return None - if snapshot.available_count <= 0: + return _select_soonest_available_credit_from_items(snapshot.credits, snapshot.available_count) + + +def _select_soonest_available_credit_from_response( + response: ResetCreditsResponse, +) -> ResetCreditItem | None: + return _select_soonest_available_credit_from_items(response.credits, response.available_count) + + +def _select_soonest_available_credit_from_items( + credits: list[ResetCreditItem], + available_count: int, +) -> ResetCreditItem | None: + if available_count <= 0: return None - available = [credit for credit in snapshot.credits if credit.status == "available"] + available = [credit for credit in credits if credit.status == "available"] if not available: return None far_future = datetime.max.replace(tzinfo=timezone.utc) @@ -206,4 +421,4 @@ def _snapshot_to_response( available_count=snapshot.available_count, nearest_expires_at=snapshot.nearest_expires_at, credits=[ResetCreditItemResponse.model_validate(credit.model_dump()) for credit in snapshot.credits], - ) + ) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..a4bdb63ec --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,14983 @@ +{ + "name": "frontend", + "version": "1.20.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.20.1", + "dependencies": { + "@hookform/resolvers": "^5.4.0", + "@tailwindcss/vite": "^4.3.1", + "@tanstack/react-query": "^5.101.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.4.0", + "input-otp": "^1.4.2", + "lucide-react": "^1.18.0", + "radix-ui": "^1.6.0", + "react": "^19.2.7", + "react-day-picker": "^10.0.1", + "react-dom": "^19.2.7", + "react-hook-form": "^7.79.0", + "react-router-dom": "^7.17.0", + "recharts": "^3.8.1", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.1", + "zod": "^4.4.3", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.61.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.3.1", + "@vitest/coverage-v8": "^4.1.9", + "eslint": "^10.5.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "msw": "^2.14.6", + "react-doctor": "^0.5.6", + "shadcn": "^4.11.0", + "tw-animate-css": "^1.4.0", + "typescript": "~6.0.3", + "typescript-eslint": "^8.61.1", + "vite": "^8.0.16", + "vitest": "^4.1.9" + } + }, + "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/@apm-js-collab/code-transformer": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.15.0.tgz", + "integrity": "sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/estree": "^1.0.8", + "astring": "^1.9.0", + "esquery": "^1.7.0", + "meriyah": "^6.1.4", + "semifies": "^1.0.0", + "source-map": "^0.6.0" + }, + "bin": { + "code-transformer": "cli.js" + } + }, + "node_modules/@apm-js-collab/code-transformer-bundler-plugins": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.5.0.tgz", + "integrity": "sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "es-module-lexer": "^2.1.0", + "magic-string": "^0.30.21", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.10.0.tgz", + "integrity": "sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "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/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "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/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.75.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", + "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@dotenvx/primitives": "^0.8.0", + "commander": "^11.1.0", + "conf": "^10.2.0", + "dotenv": "^17.2.1", + "enquirer": "^2.4.1", + "env-paths": "^2.2.1", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "open": "^8.4.2", + "picomatch": "^4.0.4", + "systeminformation": "^5.22.11", + "undici": "^7.11.0", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@dotenvx/dotenvx/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/@dotenvx/dotenvx/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@dotenvx/dotenvx/node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@dotenvx/primitives": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@dotenvx/primitives/-/primitives-0.8.0.tgz", + "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", + "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz", + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", + "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.132.0.tgz", + "integrity": "sha512-KrLaPWa5c9Y7LkW+rKkaUE3y7DBDrQtaf7rlsSDfv6KAHUjgzAIRA761Lrrp6//Yd/Rlie/yEOt9YENCoJnOcw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.132.0.tgz", + "integrity": "sha512-SThDrSeamB/kG2+NxcJ5/wSLcV6dUqDknrPLqFYQ0ST/55mtBP4M7Q/f3QbubH6aAd11wpzZn/nwbVRSdobOpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.132.0.tgz", + "integrity": "sha512-Lc0f/TYoKBghE5/2Gsv7bLXk+TJZunx2Tf61X8hG4ARXdc8UYI26dCGccFSd1AyFbK3jfaNXtMnupggDbjPXdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.132.0.tgz", + "integrity": "sha512-RG2eJIpf7C21z9HSSXFw1bTArdpKe7Y4fwcJTwRq1yCSe1vSavaN9GA1sm9KqzemTLAGVktQ+7qBTGp0vQeUZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.132.0.tgz", + "integrity": "sha512-wQIPntPLtJ8NcBpvKPbEv3NqzV6k8eP8tP/jE9Rg8HTg/j7urZGFSsTCPCW5k77Qfw2DM4vRvc9p3I4yq/Shvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.132.0.tgz", + "integrity": "sha512-PixKEpeSe3yxQWqNyOCBALRYc72+Tj7ILDofUl3iXo25cVOzLA6jHUhmOINRtWIPh7dbUie3QNeabwaQpZTw6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.132.0.tgz", + "integrity": "sha512-sCR+DzGHlyHKnbA2z9zWjTUhIo8Sy0enJl4RDsBwPmkxYynPatpwOAWe8W5127SlW0boqUWHGtr1NWn5UwIhXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.132.0.tgz", + "integrity": "sha512-sQBix5P2cW+IpzTcCwYxnh9yALrKSIkKJThspBvMGcygSMnbzkSvhN7SfuX1hvBk8y1XEChsdkU3ET0V5DmzUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.132.0.tgz", + "integrity": "sha512-WozHg3Kc//8Sk756HXXgMbEAvqtG+Lzb9JOojwQzIGDtN78Az2dLttkb71akWYUF/8IgYfDSlfKh4Uot8is5Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.132.0.tgz", + "integrity": "sha512-CmX/ulNBOEwWTyVRmcpYKAcAizW6+OjtLJgo7fXoL9OqQvjF4VER8tPomv44vwzfSCy1BHbsB0ZlZYzYJNj4cA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.132.0.tgz", + "integrity": "sha512-j9oQS+hM90SdhviNGWbPgT4+Rlq+ac++q/zjgwPD1mVHgxHzATvoRGtDx0sXGmFOQ9J9YkwAhYGb5MAHL6TAsA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.132.0.tgz", + "integrity": "sha512-bLz+Xi+Agnfmd7kWPEsSVwCn2k4EyIalZkNBcQ0OGIv9rqn8VgCPLNd03tM9mKX/5TdlvDXalz0q71BIrOPNqg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.132.0.tgz", + "integrity": "sha512-U6t2qbJU0ypTfyj9QV3W1Y6mITDTL8ai/OR6NUn85vyHthOvobKWgXzU4tu0EskSzlpuVFz1g0jFGulDIUKHxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.132.0.tgz", + "integrity": "sha512-WcEaSNHFk8yz5YFlQQAlhq6jOFmZBB/RKE7uzhyCIf+pF1Lmv9gUH4221mle2Gd9iHyWT3ySNph8yZgb1xYdWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.132.0.tgz", + "integrity": "sha512-iQrV4iJzQgRwK3BWRmQl1C3C6g3wYpXN2WLdQdyR+efoUnncdShZAVp9OgcojtlD3MDRbuOMGG3SjxF4fL4nlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.132.0.tgz", + "integrity": "sha512-FWzmUGrZ6GUby4U7WIwcCtab6tdmlTO3xTRRKyb5kjIJVEiaUAT8animUG/nK8ZCA8gkRkPOTId4rl6uTqUmJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.132.0.tgz", + "integrity": "sha512-TlbMppxJI5CjWDes0QaP6G3aneVg1yikBu5QYI+DUShF9WDL66ccgKFNNGmi/Wybtszw6hxwAvv76T4DaPKnHw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.132.0.tgz", + "integrity": "sha512-RH/NbFjGKqdUAUi7Oh3LQPxUk2hsWFEEQ38HSnbRQT8QjBZFKqL1fMbmsB3N4jy/KPh9iX94+9dmkEMBBbambw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.132.0.tgz", + "integrity": "sha512-JUr4jQY9jxoIB/YTLXr6XofSi5xikj6p5/Ns1h0VOBDT0j1jKU+kMsv2xxv51RwnETcXpA1Yw/9oUAfcqfaqEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.132.0.tgz", + "integrity": "sha512-2dapgHpA5X8DSXF4AU36hJWYf6zP0tKjMXFRAZFBD62pkevW/uhFDXoFH9Y/3Fd2EtDrw5ByNnR1wVE9X9y0SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", + "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", + "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", + "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", + "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", + "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", + "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", + "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", + "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", + "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", + "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", + "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", + "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", + "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", + "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", + "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", + "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", + "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", + "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", + "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.66.0.tgz", + "integrity": "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.66.0.tgz", + "integrity": "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.66.0.tgz", + "integrity": "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.66.0.tgz", + "integrity": "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.66.0.tgz", + "integrity": "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.66.0.tgz", + "integrity": "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.66.0.tgz", + "integrity": "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.66.0.tgz", + "integrity": "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.66.0.tgz", + "integrity": "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.66.0.tgz", + "integrity": "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.66.0.tgz", + "integrity": "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.66.0.tgz", + "integrity": "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.66.0.tgz", + "integrity": "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.66.0.tgz", + "integrity": "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.66.0.tgz", + "integrity": "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.66.0.tgz", + "integrity": "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.66.0.tgz", + "integrity": "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.66.0.tgz", + "integrity": "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.66.0.tgz", + "integrity": "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", + "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.10.tgz", + "integrity": "sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.14.tgz", + "integrity": "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collapsible": "1.1.14", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.17.tgz", + "integrity": "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dialog": "1.1.17", + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", + "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.10.tgz", + "integrity": "sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.0.tgz", + "integrity": "sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.5.tgz", + "integrity": "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz", + "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", + "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", + "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.1.tgz", + "integrity": "sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz", + "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", + "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.18.tgz", + "integrity": "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", + "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.10.tgz", + "integrity": "sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-label": "2.1.10", + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.17.tgz", + "integrity": "sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.10.tgz", + "integrity": "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.18.tgz", + "integrity": "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.18.tgz", + "integrity": "sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.16.tgz", + "integrity": "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.10.tgz", + "integrity": "sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.5.tgz", + "integrity": "sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz", + "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", + "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", + "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.10.tgz", + "integrity": "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.1.tgz", + "integrity": "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", + "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz", + "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz", + "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.10.tgz", + "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.1.tgz", + "integrity": "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.1.tgz", + "integrity": "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", + "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.17.tgz", + "integrity": "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.12.tgz", + "integrity": "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.13.tgz", + "integrity": "sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-toggle": "1.1.12", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.13.tgz", + "integrity": "sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-separator": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz", + "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", + "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", + "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", + "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz", + "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", + "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "extraneous": true, + "libc": [ + "musl" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "libc": [ + "musl" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "extraneous": true, + "libc": [ + "musl" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "libc": [ + "musl" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "extraneous": true, + "libc": [ + "musl" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "extraneous": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "libc": [ + "musl" + ], + "license": "MIT", + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sentry/conventions": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.12.0.tgz", + "integrity": "sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.59.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.59.0.tgz", + "integrity": "sha512-QeG7XZL5j6CkToYCE7OwCerb/r742Tjj9p1BBohBKcypYTPRuqfD+A3FeUj7pk5CGO6Vj1/gOAmdbuuNbR51dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.59.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.59.0.tgz", + "integrity": "sha512-qzqbP6OVoMijlDBUxWtbvVF5j73+vyzGFi+yFIslhVvzBj97TFkIeP3TpBLsmu/0L5ZvxpQCCEmzJ677tFkq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/core": "^2.6.1", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/sdk-trace-base": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@sentry/core": "10.59.0", + "@sentry/node-core": "10.59.0", + "@sentry/opentelemetry": "10.59.0", + "@sentry/server-utils": "10.59.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.59.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.59.0.tgz", + "integrity": "sha512-qFbepzntYhDleNG9ZCZWCSoAJK0Nsx+UJxsuiygaaAf1rJMj95RVckLyslhY86pyDLVATNMmWm2elm6etgKaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.59.0", + "@sentry/opentelemetry": "10.59.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/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" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/server-utils": { + "version": "10.59.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.59.0.tgz", + "integrity": "sha512-mR3fWaU7uGxIstRba6YO+/6V3qIa7432F7/U8EWHry+dY4C9DWAVG90E2GCzeD2MwLSP0tB25i8p1TWTGiQgVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", + "@apm-js-collab/tracing-hooks": "^0.10.0", + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.59.0", + "magic-string": "~0.30.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/@sentry/node/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "extraneous": 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/@sentry/node/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@sentry/node/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "extraneous": true, + "hasInstallScript": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@sentry/node/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sentry/node/node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "extraneous": 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/@sentry/opentelemetry": { + "version": "10.59.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.59.0.tgz", + "integrity": "sha512-wV9/HR9btrNhSkJC2S0urqsD9pE4K0f6AmdfTK3qhH505mLoyV4ekTG66hdDR9xD2zOYCm58CNzaK+336zu3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.59.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz", + "integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.41", + "@swc/core-darwin-x64": "1.15.41", + "@swc/core-linux-arm-gnueabihf": "1.15.41", + "@swc/core-linux-arm64-gnu": "1.15.41", + "@swc/core-linux-arm64-musl": "1.15.41", + "@swc/core-linux-ppc64-gnu": "1.15.41", + "@swc/core-linux-s390x-gnu": "1.15.41", + "@swc/core-linux-x64-gnu": "1.15.41", + "@swc/core-linux-x64-musl": "1.15.41", + "@swc/core-win32-arm64-msvc": "1.15.41", + "@swc/core-win32-ia32-msvc": "1.15.41", + "@swc/core-win32-x64-msvc": "1.15.41" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz", + "integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz", + "integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz", + "integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz", + "integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz", + "integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz", + "integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz", + "integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz", + "integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz", + "integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz", + "integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz", + "integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz", + "integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "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", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "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/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/jest-dom/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/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.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", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.1.tgz", + "integrity": "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0", + "@swc/core": "^1.15.11" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.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/agent-install": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/agent-install/-/agent-install-0.0.5.tgz", + "integrity": "sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "commander": "^14.0.0", + "jsonc-parser": "^3.3.1", + "picocolors": "^1.1.1", + "prompts": "^2.4.2", + "yaml": "^2.8.3" + }, + "bin": { + "agent-install": "bin/agent-install.mjs" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.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/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/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/conf": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.1.0.tgz", + "integrity": "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/conf/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deslop-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/deslop-js/-/deslop-js-0.5.8.tgz", + "integrity": "sha512-Vq9D2x4dAIW24zcH55DTrl3/vi13UNKfXgw0yj7ULTssZ6KOdw/oyBHtlvE94KFC9yYEhgFTrGjaqqZKvV9pwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.132.0", + "fast-glob": "^3.3.3", + "minimatch": "^10.2.5", + "oxc-parser": "^0.132.0", + "oxc-resolver": "^11.19.1", + "typescript": "^6.0.3" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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", + "peer": true + }, + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-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": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "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-toolkit": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.48.1.tgz", + "integrity": "sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "extraneous": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "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/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hono": { + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-in-the-middle": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.1.0.tgz", + "integrity": "sha512-c0AeAV8VcwZzfYE7euTZY3H+VXUPMVugiovdosq80lqEXJmOekg3zGUAYg6KImHMaMuBoTUfTv7xNpUFdy0hJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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", + "peer": true, + "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/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "dev": true, + "license": "MIT" + }, + "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/msw": { + "version": "2.14.6", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.6.tgz", + "integrity": "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/oxc-parser": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.132.0.tgz", + "integrity": "sha512-+0LAPHaqtfQlvWdpaAa09SmOaZZgP8C552xosEkGJ4+ruEwP1Vgx+sqBgcBCNfR6KDCmagGOZTde8wmAvcI/Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.132.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.132.0", + "@oxc-parser/binding-android-arm64": "0.132.0", + "@oxc-parser/binding-darwin-arm64": "0.132.0", + "@oxc-parser/binding-darwin-x64": "0.132.0", + "@oxc-parser/binding-freebsd-x64": "0.132.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.132.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.132.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.132.0", + "@oxc-parser/binding-linux-arm64-musl": "0.132.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.132.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.132.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.132.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.132.0", + "@oxc-parser/binding-linux-x64-gnu": "0.132.0", + "@oxc-parser/binding-linux-x64-musl": "0.132.0", + "@oxc-parser/binding-openharmony-arm64": "0.132.0", + "@oxc-parser/binding-wasm32-wasi": "0.132.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.132.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.132.0", + "@oxc-parser/binding-win32-x64-msvc": "0.132.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", + "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.3", + "@oxc-resolver/binding-android-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-x64": "11.21.3", + "@oxc-resolver/binding-freebsd-x64": "11.21.3", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-musl": "11.21.3", + "@oxc-resolver/binding-openharmony-arm64": "11.21.3", + "@oxc-resolver/binding-wasm32-wasi": "11.21.3", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + } + }, + "node_modules/oxlint": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.66.0.tgz", + "integrity": "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.66.0", + "@oxlint/binding-android-arm64": "1.66.0", + "@oxlint/binding-darwin-arm64": "1.66.0", + "@oxlint/binding-darwin-x64": "1.66.0", + "@oxlint/binding-freebsd-x64": "1.66.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.66.0", + "@oxlint/binding-linux-arm-musleabihf": "1.66.0", + "@oxlint/binding-linux-arm64-gnu": "1.66.0", + "@oxlint/binding-linux-arm64-musl": "1.66.0", + "@oxlint/binding-linux-ppc64-gnu": "1.66.0", + "@oxlint/binding-linux-riscv64-gnu": "1.66.0", + "@oxlint/binding-linux-riscv64-musl": "1.66.0", + "@oxlint/binding-linux-s390x-gnu": "1.66.0", + "@oxlint/binding-linux-x64-gnu": "1.66.0", + "@oxlint/binding-linux-x64-musl": "1.66.0", + "@oxlint/binding-openharmony-arm64": "1.66.0", + "@oxlint/binding-win32-arm64-msvc": "1.66.0", + "@oxlint/binding-win32-ia32-msvc": "1.66.0", + "@oxlint/binding-win32-x64-msvc": "1.66.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.22.1" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } + } + }, + "node_modules/oxlint-plugin-react-doctor": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/oxlint-plugin-react-doctor/-/oxlint-plugin-react-doctor-0.5.8.tgz", + "integrity": "sha512-L0jveKAMbqF1qAqA2Ksu8aH0/Q8FDQxLwXmHYgALa2XlsxEUuamJ+1Da2MhPWJ2ahn+ekFbnWK20qixxD+fw6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.59.3", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "oxc-parser": "^0.135.0" + }, + "engines": { + "node": "^20.19.0 || >=22.13.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.135.0.tgz", + "integrity": "sha512-sHeZItACNcA5WRAWqF6ixriR4GkZDyY10gVgnZU7pXku1DjHFATSqnwZM809jl0gXPHxb6fKzYQCK7bNK5cACQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.135.0.tgz", + "integrity": "sha512-wPte+SzgzWWFgMSF8YZDNM+tBXtJg0AXBi7+tU3yS2z1f2Af9kRLZLKuJojADmuD/cZexmnMHHC3SDItTW77Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.135.0.tgz", + "integrity": "sha512-BmKz3lHIsqVos+9aPcdYCT9MG3APoUyM43KlEFhJMWNVDOGG8FKyiFz81Bc+mGz2o0hpuQ3PfXLfVWJrKXjo2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.135.0.tgz", + "integrity": "sha512-dM8BS+8+Br1fNvmh2QZbGiHaYttwLebRa6J4Uz9vuFzMNmvsdRYwf7993ptOaV0JTrR63AaoVLjX7nhWbijxjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.135.0.tgz", + "integrity": "sha512-xlZnvvJdR9bGu2pOhvR5hMuKPHCE6Sa9owK5A484mzjHdm75VRV5nCs5w/jkmGODMMTFc+KN7EnZqEieM813kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.135.0.tgz", + "integrity": "sha512-PSR8LmBK/H/PQRiN8g7RebQgZX/ntVCrdT/JBfNxE5ezdHG1s2i4rbazsRJYD83TTI1MmgTpC0MGL42PLtskQQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.135.0.tgz", + "integrity": "sha512-I85GJXzfUsigkkk7Ngdz95C217M4FdUi1Z2HrX5UyPmURobwQZ7m2bbUvwFkz4VGZd+lymFGKHvDZ3RQC9qOzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.135.0.tgz", + "integrity": "sha512-zqEY0npz0g0aGZj/8a5BclunjVDytsBQHYtIC10Gd26HcrLwbVF6YDbqRQjunMGYdSo97u6xOBl05aTDI2diDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.135.0.tgz", + "integrity": "sha512-mWAfprP819gQ2qYst1RxgTI8b/z0b29OpoKfRflIXLHde2dZLihQD4g47Onuvtpo5GPIkMYPRlX9QoeZfs/GnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.135.0.tgz", + "integrity": "sha512-gri8c2AOmJKJwOux2KTHFBfUaXoJURuVMKhmKEi/2hTF55cQteTDV2XNfTiE5oCC+Tnem1Y4/MWzcyDadtsSag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.135.0.tgz", + "integrity": "sha512-Y2tkupCG5wo0SxH2rMLG4d4Kmv6DaM3sBp+GuM5lox0S8Za6VxKgQrY2Mut088QQxKkEE89n/4CCCgmw2o0e3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.135.0.tgz", + "integrity": "sha512-xDRJq6i6WTynjeP+ISbDpyH4p9BaJ0wuQcL0lCSDkt9qOXC9dmwpOu1VG/TlwmPI3KpYntmO9nJCuc3TMTsNBA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.135.0.tgz", + "integrity": "sha512-V4MoUuiCRNvihxhIufRxvK+ka013V4joTSK0FAGA1KEjLuNprfH6N/Qw2uxQEVIFuNYMhD/hV6xJ/ptbzlKdHg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.135.0.tgz", + "integrity": "sha512-JCFZ7zM7KXOKoPAbK/ZB4wY0M1jxRECiem2UQuiXLjzGqS9+hno7mtX+qyK2F7HWK2xPhyJb+frpcOtk5DKOtg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.135.0.tgz", + "integrity": "sha512-9jSVS1b3hOV7sdKH4aA2DFfnTz0RgQd0v2BefR+LYbH8yIlmSM22JJZbAAjVeVXmFgUAk3zJQ1tpE/Nd+Vi2YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.135.0.tgz", + "integrity": "sha512-M857ZLBSdn1Uy/SJJz5zh0qGu67B4P9omCgXGBU2LLqTzraX6ZjVNaKq5yW1PDw/LgJXDXR/dbZfgmB310f11Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.135.0.tgz", + "integrity": "sha512-2w6DVcntQZX9U5RhXtgiWb3FLWFB5EcwI1U8yr3htOCJUJjagN4BFUHz/Y/d9ZsumndZ6ByxxWEtbUZNE1bfFw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.135.0.tgz", + "integrity": "sha512-rX1U8+IH2Z37EJjDXKa1iifvUQAdba+vZ4Ewj1iaG5eA/QaSybzclCOwtWa0/5BuUQnnK/T2JHUEFrwhL6Ck2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.135.0.tgz", + "integrity": "sha512-9FAisBbH1QICGAjlJobiuKGd/jOuVmyqniWdQMwTa5SkCl6hhuotBCJf1n46B0flYbSOR5TzfV9HZCWSyb3c/Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.135.0.tgz", + "integrity": "sha512-wYF+A2AzJ2n7ul6q+Z2G/ia0S2+8cUp0AgWZzoFvF4WmUcl1P7p+o6se1Gdr5wGnWuF0iAMIkGddrjCarNr2yA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/@oxc-project/types": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.135.0.tgz", + "integrity": "sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/oxlint-plugin-react-doctor/node_modules/oxc-parser": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.135.0.tgz", + "integrity": "sha512-/DaPStu0s2zzNSRRniKyTPM6Z/o+DapOp2JYNKDL8AsgaBGPK2IdZyB87SQjVH+xeQPz+Qr9mrjglfkYgtbVRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.135.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.135.0", + "@oxc-parser/binding-android-arm64": "0.135.0", + "@oxc-parser/binding-darwin-arm64": "0.135.0", + "@oxc-parser/binding-darwin-x64": "0.135.0", + "@oxc-parser/binding-freebsd-x64": "0.135.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.135.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.135.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.135.0", + "@oxc-parser/binding-linux-arm64-musl": "0.135.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.135.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.135.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.135.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.135.0", + "@oxc-parser/binding-linux-x64-gnu": "0.135.0", + "@oxc-parser/binding-linux-x64-musl": "0.135.0", + "@oxc-parser/binding-openharmony-arm64": "0.135.0", + "@oxc-parser/binding-wasm32-wasi": "0.135.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.135.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.135.0", + "@oxc-parser/binding-win32-x64-msvc": "0.135.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-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", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/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", + "peer": true + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/radix-ui": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.0.tgz", + "integrity": "sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-accessible-icon": "1.1.10", + "@radix-ui/react-accordion": "1.2.14", + "@radix-ui/react-alert-dialog": "1.1.17", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-aspect-ratio": "1.1.10", + "@radix-ui/react-avatar": "1.2.0", + "@radix-ui/react-checkbox": "1.3.5", + "@radix-ui/react-collapsible": "1.1.14", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-context-menu": "2.3.1", + "@radix-ui/react-dialog": "1.1.17", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-dropdown-menu": "2.1.18", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-form": "0.1.10", + "@radix-ui/react-hover-card": "1.1.17", + "@radix-ui/react-label": "2.1.10", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-menubar": "1.1.18", + "@radix-ui/react-navigation-menu": "1.2.16", + "@radix-ui/react-one-time-password-field": "0.1.10", + "@radix-ui/react-password-toggle-field": "0.1.5", + "@radix-ui/react-popover": "1.1.17", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-progress": "1.1.10", + "@radix-ui/react-radio-group": "1.4.1", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-scroll-area": "1.2.12", + "@radix-ui/react-select": "2.3.1", + "@radix-ui/react-separator": "1.1.10", + "@radix-ui/react-slider": "1.4.1", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-switch": "1.3.1", + "@radix-ui/react-tabs": "1.1.15", + "@radix-ui/react-toast": "1.2.17", + "@radix-ui/react-toggle": "1.1.12", + "@radix-ui/react-toggle-group": "1.1.13", + "@radix-ui/react-toolbar": "1.1.13", + "@radix-ui/react-tooltip": "1.2.10", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-escape-keydown": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.1.tgz", + "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-doctor": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/react-doctor/-/react-doctor-0.5.8.tgz", + "integrity": "sha512-gDXDQ+48KeFq2jkVgsUhQ67oQ+kUMdnIaA+YeS9VXXZFXSg9wxY5dDxurEpILSh8RwO06W8p6uCnTR+wn5B/GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@sentry/node": "^10.54.0", + "agent-install": "0.0.5", + "conf": "^15.1.0", + "confbox": "^0.2.4", + "deslop-js": "0.5.8", + "eslint-plugin-react-hooks": "^7.1.1", + "jiti": "^2.7.0", + "magicast": "^0.5.3", + "oxlint": ">=1.66.0 <1.67.0", + "oxlint-plugin-react-doctor": "0.5.8", + "prompts": "^2.4.2", + "typescript": ">=5.0.4 <7", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-uri": "^3.1.0" + }, + "bin": { + "react-doctor": "bin/react-doctor.js" + }, + "engines": { + "node": "^20.19.0 || >=22.13.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-hook-form": { + "version": "7.80.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.80.0.tgz", + "integrity": "sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "extraneous": 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.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shadcn": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.11.0.tgz", + "integrity": "sha512-UV0cchFea9hO7poV1CuEP0wvmYjpAqcxCKdy23bndl2Du2ARtDs8A4xdzfhUjDBeOW1nNpJ6lXmsEpsply2SfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "@modelcontextprotocol/sdk": "^1.26.0", + "@types/validate-npm-package-name": "^4.0.2", + "browserslist": "^4.26.2", + "commander": "^14.0.0", + "cosmiconfig": "^9.0.0", + "dedent": "^1.6.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "https-proxy-agent": "^7.0.6", + "kleur": "^4.1.5", + "node-fetch": "^3.3.2", + "open": "^11.0.0", + "ora": "^8.2.0", + "postcss": "^8.5.6", + "postcss-selector-parser": "^7.1.0", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "stringify-object": "^5.0.0", + "tailwind-merge": "^3.0.1", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "validate-npm-package-name": "^7.0.1", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.6" + }, + "bin": { + "shadcn": "dist/index.js" + } + }, + "node_modules/shadcn/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/shadcn/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/systeminformation": { + "version": "5.31.7", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.7.tgz", + "integrity": "sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "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": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.3" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-morph": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", + "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "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/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-spinner": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.0.tgz", + "integrity": "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/src/components/layout/app-header.test.tsx b/frontend/src/components/layout/app-header.test.tsx index 5c45a3c2c..60d04eab8 100644 --- a/frontend/src/components/layout/app-header.test.tsx +++ b/frontend/src/components/layout/app-header.test.tsx @@ -44,6 +44,25 @@ describe("AppHeader", () => { expect(await screen.findAllByText("99+")).not.toHaveLength(0); }); + it("sums reset-credit badge across accounts and treats missing counts as zero", async () => { + server.use( + http.get("/api/accounts", () => + HttpResponse.json({ + accounts: [ + createAccountSummary({ availableResetCredits: 5 }), + createAccountSummary({ accountId: "acc-2" }), + createAccountSummary({ accountId: "acc-3", availableResetCredits: null }), + createAccountSummary({ accountId: "acc-4", availableResetCredits: 3 }), + ], + }), + ), + ); + + renderHeader(); + + expect(await screen.findAllByText("8")).not.toHaveLength(0); + }); + it("hides the Accounts reset-credit badge when no resets are available", async () => { server.use( http.get("/api/accounts", () => diff --git a/frontend/src/features/accounts/components/account-actions.test.tsx b/frontend/src/features/accounts/components/account-actions.test.tsx index b08f8c1b9..54df9afc1 100644 --- a/frontend/src/features/accounts/components/account-actions.test.tsx +++ b/frontend/src/features/accounts/components/account-actions.test.tsx @@ -187,6 +187,41 @@ describe("AccountActions", () => { expect(onResetCredit).toHaveBeenCalledWith(account.accountId); }); + it.each(["paused", "deactivated", "reauth_required"] as const)( + "disables reset action for %s accounts", + async (status) => { + const user = userEvent.setup(); + const onResetCredit = vi.fn(); + const account = createAccountSummary({ + status, + availableResetCredits: 2, + resetCreditNearestExpiresAt: "2026-01-03T12:00:00.000Z", + }); + + render( + , + ); + + const button = screen.getByRole("button", { name: "Reset (2)" }); + expect(button).toBeDisabled(); + await user.click(button); + expect(onResetCredit).not.toHaveBeenCalled(); + }, + ); + it("hides reset action when no reset credits are available", () => { const account = createAccountSummary({ availableResetCredits: 0, diff --git a/frontend/src/features/accounts/components/account-actions.tsx b/frontend/src/features/accounts/components/account-actions.tsx index ab096ed61..a66f73ac0 100644 --- a/frontend/src/features/accounts/components/account-actions.tsx +++ b/frontend/src/features/accounts/components/account-actions.tsx @@ -69,6 +69,11 @@ export function AccountActions({ : null; const availableResetCredits = account.availableResetCredits ?? 0; const hasResetCredits = availableResetCredits > 0; + const resetCreditDisabled = + busy || + readOnly || + account.status === "paused" || + showOperatorRecoveryAction; return (
@@ -207,7 +212,7 @@ export function AccountActions({ variant="outline" className="relative h-8 gap-1.5 pr-8 text-xs" onClick={() => onResetCredit(account.accountId)} - disabled={busy || readOnly} + disabled={resetCreditDisabled} > {`Reset (${availableResetCredits})`} diff --git a/frontend/src/features/accounts/components/accounts-page.tsx b/frontend/src/features/accounts/components/accounts-page.tsx index c97b47d89..48b6cc1d1 100644 --- a/frontend/src/features/accounts/components/accounts-page.tsx +++ b/frontend/src/features/accounts/components/accounts-page.tsx @@ -54,7 +54,8 @@ export function AccountsPage() { const importDialog = useDialogState(); const oauthDialog = useDialogState(); const deleteDialog = useDialogState(); - const resetCreditDialog = useDialogState(); + type ResetCreditDialogTarget = { accountId: string; availableResetCredits: number }; + const resetCreditDialog = useDialogState(); const exportDialog = useDialogState(); const [deleteHistory, setDeleteHistory] = useState(false); @@ -180,7 +181,13 @@ export function AccountsPage() { .then((result) => exportDialog.show(result)) .catch(() => null); }} - onResetCredit={(accountId) => resetCreditDialog.show(accountId)} + onResetCredit={(accountId) => { + const account = accountsQuery.data?.find((item) => item.accountId === accountId); + resetCreditDialog.show({ + accountId, + availableResetCredits: account?.availableResetCredits ?? 0, + }); + }} onLimitWarmupChange={(accountId, enabled) => void limitWarmupMutation.mutateAsync({ accountId, enabled }) } @@ -241,7 +248,8 @@ export function AccountsPage() { {resetCreditDialog.data ? ( ) : null} diff --git a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx index deecb5655..ba87e34a2 100644 --- a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx +++ b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.test.tsx @@ -93,8 +93,10 @@ describe("ResetCreditConfirmDialog", () => { await vi.waitFor(() => expect(toastSuccess).toHaveBeenCalledWith("Rate-limit window reset (1)"), ); - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["accounts"] }); - expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["dashboard"] }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["accounts", "list"] }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["accounts", "trends"] }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["dashboard", "overview"] }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["dashboard", "projections"] }); expect(onOpenChange).toHaveBeenCalledWith(false); }); @@ -132,12 +134,75 @@ describe("ResetCreditConfirmDialog", () => { await vi.waitFor(() => expect(toastError).toHaveBeenCalledWith("No reset credit available"), ); - expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["accounts"] }); - expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["dashboard"] }); + expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["accounts", "list"] }); + expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: ["dashboard", "overview"] }); // Failure leaves the dialog open for retry. expect(onOpenChange).not.toHaveBeenCalledWith(false); }); + it("shows a loading state while the reset-credit snapshot is fetching", () => { + server.use( + http.get(SNAPSHOT_URL, async () => { + await new Promise(() => {}); + return snapshotResponse(); + }), + ); + + renderWithClient( + , + ); + + expect(screen.getByText("Loading reset credit details...")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Redeem credit" })).toBeDisabled(); + }); + + it("shows an error message and keeps confirm disabled when the snapshot fetch fails", async () => { + server.use( + http.get(SNAPSHOT_URL, () => + HttpResponse.json( + { + error: { + code: "service_unavailable", + message: "Reset credits unavailable", + }, + }, + { status: 503 }, + ), + ), + ); + + renderWithClient( + , + ); + + expect(await screen.findByText("Reset credits unavailable")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Redeem credit" })).toBeDisabled(); + }); + + it("handles a null snapshot response without allowing redeem", async () => { + server.use(http.get(SNAPSHOT_URL, () => HttpResponse.json(null))); + + renderWithClient( + , + ); + + expect(await screen.findByText("0 free rate limit resets")).toBeInTheDocument(); + expect(screen.getByText("Reset credit details are not available yet.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Redeem credit" })).toBeDisabled(); + }); + it("allows redeeming an available credit when expiry is null", async () => { const user = userEvent.setup(); const consumeCalled = vi.fn(); @@ -186,4 +251,36 @@ describe("ResetCreditConfirmDialog", () => { await vi.waitFor(() => expect(consumeCalled).toHaveBeenCalledTimes(1)); }); + + it("enables redeem when GET cache is empty but summary reports available credits", async () => { + const user = userEvent.setup(); + const consumeCalled = vi.fn(); + server.use( + http.get(SNAPSHOT_URL, () => HttpResponse.json(null)), + http.post(CONSUME_URL, () => { + consumeCalled(); + return HttpResponse.json({ + code: "rate_limit_reset", + windowsReset: 1, + redeemedAt: "2026-01-01T12:00:00.000Z", + }); + }), + ); + + renderWithClient( + , + ); + + expect(await screen.findByText("2 free rate limit resets")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Redeem credit" })).toBeEnabled(); + + await user.click(screen.getByRole("button", { name: "Redeem credit" })); + + await vi.waitFor(() => expect(consumeCalled).toHaveBeenCalledTimes(1)); + }); }); diff --git a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx index c6a1becc4..0b99f8326 100644 --- a/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx +++ b/frontend/src/features/accounts/components/reset-credit-confirm-dialog.tsx @@ -5,12 +5,15 @@ import { } from "@/features/accounts/hooks/use-accounts"; import type { RateLimitResetCreditItem } from "@/features/accounts/schemas"; import { cn } from "@/lib/utils"; +import { getErrorMessage } from "@/utils/errors"; import { formatLocalDateTimeSeconds, formatSingleUnitRemaining } from "@/utils/formatters"; export type ResetCreditConfirmDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; accountId: string | null; + /** Count from account summary when the per-account cache GET has not populated yet. */ + summaryAvailableCount?: number; }; function pickSoonestAvailableCredit( @@ -70,15 +73,27 @@ export function ResetCreditConfirmDialog({ open, onOpenChange, accountId, + summaryAvailableCount = 0, }: ResetCreditConfirmDialogProps) { const { resetCreditConsumeMutation } = useAccountMutations(); const snapshotQuery = useRateLimitResetCredits(accountId, open); + const snapshotLoading = snapshotQuery.isPending; + const snapshotError = snapshotQuery.isError; + const snapshotErrorMessage = getErrorMessage( + snapshotQuery.error, + "Failed to load reset credit details", + ); const soonest = pickSoonestAvailableCredit(snapshotQuery.data?.credits); const otherCredits = (snapshotQuery.data?.credits ?? []).filter( (c) => c.status === "available" && c.id !== soonest?.id, ); - const availableCount = snapshotQuery.data?.availableCount ?? 0; + const availableCount = + snapshotQuery.data != null + ? snapshotQuery.data.availableCount + : summaryAvailableCount; const pending = resetCreditConsumeMutation.isPending; + const confirmDisabled = + pending || !accountId || snapshotLoading || snapshotError || availableCount <= 0; const handleConfirm = () => { if (!accountId || pending) { @@ -111,36 +126,48 @@ export function ResetCreditConfirmDialog({ description="This redeems the soonest-expiring banked reset credit for this account." confirmLabel={pending ? "Redeeming..." : "Redeem credit"} cancelLabel="Cancel" - confirmDisabled={pending || !accountId || !soonest} + confirmDisabled={confirmDisabled} onOpenChange={handleOpenChange} onConfirm={handleConfirm} >
-

- {availableCount} free rate limit reset{availableCount !== 1 ? "s" : ""} -

- {soonest ? ( -
- - {otherCredits.map((credit) => ( - - ))} - {!soonest.expiresAt && otherCredits.length === 0 ? ( -

No upcoming expiry data available.

+ {snapshotLoading ? ( +

Loading reset credit details...

+ ) : snapshotError ? ( +

{snapshotErrorMessage}

+ ) : ( + <> +

+ {availableCount} free rate limit reset{availableCount !== 1 ? "s" : ""} +

+ {soonest ? ( +
+ + {otherCredits.map((credit) => ( + + ))} + {!soonest.expiresAt && otherCredits.length === 0 ? ( +

No upcoming expiry data available.

+ ) : null} +
+ ) : availableCount > 0 ? ( +

No upcoming expiry data available.

+ ) : snapshotQuery.data === null ? ( +

+ Reset credit details are not available yet. +

) : null} -
- ) : availableCount > 0 ? ( -

No upcoming expiry data available.

- ) : null} + + )}
); diff --git a/frontend/src/features/accounts/hooks/use-accounts.ts b/frontend/src/features/accounts/hooks/use-accounts.ts index 3672be4ec..616063bdf 100644 --- a/frontend/src/features/accounts/hooks/use-accounts.ts +++ b/frontend/src/features/accounts/hooks/use-accounts.ts @@ -182,12 +182,15 @@ export function useAccountMutations() { const resetCreditConsumeMutation = useMutation({ mutationFn: (accountId: string) => consumeRateLimitResetCredit(accountId), onSuccess: (data) => { - const resetCount = data.windowsReset; + const resetCount = data.windowsReset ?? 0; toast.success( `Rate-limit window${resetCount === 1 ? "" : "s"} reset (${resetCount})`, ); - void queryClient.invalidateQueries({ queryKey: ["accounts"] }); - void queryClient.invalidateQueries({ queryKey: ["dashboard"] }); + void queryClient.invalidateQueries({ queryKey: ["accounts", "list"] }); + void queryClient.invalidateQueries({ queryKey: ["accounts", "trends"] }); + void queryClient.invalidateQueries({ queryKey: ["accounts", "reset-credits"] }); + void queryClient.invalidateQueries({ queryKey: ["dashboard", "overview"] }); + void queryClient.invalidateQueries({ queryKey: ["dashboard", "projections"] }); }, onError: (error: Error) => { toast.error(error.message || "Reset credit redeem failed"); diff --git a/frontend/src/features/accounts/schemas.test.ts b/frontend/src/features/accounts/schemas.test.ts index 900755b00..014586d91 100644 --- a/frontend/src/features/accounts/schemas.test.ts +++ b/frontend/src/features/accounts/schemas.test.ts @@ -214,52 +214,25 @@ describe("RateLimitResetCreditsSnapshotSchema", () => { }); describe("ConsumeRateLimitResetCreditResponseSchema", () => { - it("parses successful consume responses with optional redeemedAt", () => { + it("parses consume responses when nullable backend fields are omitted or null", () => { expect( ConsumeRateLimitResetCreditResponseSchema.parse({ - code: "rate_limit_reset", - windowsReset: 1, redeemedAt: ISO, }), ).toMatchObject({ - code: "rate_limit_reset", - windowsReset: 1, redeemedAt: ISO, }); expect( ConsumeRateLimitResetCreditResponseSchema.parse({ - code: "rate_limit_reset", - windowsReset: 1, + code: null, + windowsReset: null, redeemedAt: null, }), ).toMatchObject({ - code: "rate_limit_reset", - windowsReset: 1, + code: null, + windowsReset: null, redeemedAt: null, }); - - expect( - ConsumeRateLimitResetCreditResponseSchema.parse({ - code: "rate_limit_reset", - windowsReset: 1, - }), - ).toMatchObject({ - code: "rate_limit_reset", - windowsReset: 1, - }); - - expect(() => - ConsumeRateLimitResetCreditResponseSchema.parse({ - redeemedAt: ISO, - }), - ).toThrow(); - - expect(() => - ConsumeRateLimitResetCreditResponseSchema.parse({ - code: null, - windowsReset: null, - }), - ).toThrow(); }); }); diff --git a/frontend/src/features/accounts/schemas.ts b/frontend/src/features/accounts/schemas.ts index a62af06e4..378b9cca3 100644 --- a/frontend/src/features/accounts/schemas.ts +++ b/frontend/src/features/accounts/schemas.ts @@ -114,9 +114,9 @@ export const RateLimitResetCreditsSnapshotSchema = z.object({ }); export const ConsumeRateLimitResetCreditResponseSchema = z.object({ - code: z.string(), - windowsReset: z.number(), - redeemedAt: z.iso.datetime({ offset: true }).nullable().optional(), + code: z.string().nullable().optional(), + windowsReset: z.number().nullable().optional(), + redeemedAt: z.iso.datetime({ offset: true }).nullable(), }); export const AccountTrendsResponseSchema = z.object({ diff --git a/frontend/src/features/dashboard/components/account-card.test.tsx b/frontend/src/features/dashboard/components/account-card.test.tsx index ad3c47c1d..06cae0835 100644 --- a/frontend/src/features/dashboard/components/account-card.test.tsx +++ b/frontend/src/features/dashboard/components/account-card.test.tsx @@ -1,5 +1,6 @@ import { act, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, it } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { AccountCard } from "@/features/dashboard/components/account-card"; import { usePrivacyStore } from "@/hooks/use-privacy"; @@ -127,4 +128,24 @@ describe("AccountCard", () => { expect(screen.queryByRole("button", { name: /Reset \(/ })).not.toBeInTheDocument(); }); + + it("disables reset action for paused accounts", async () => { + const user = userEvent.setup(); + const onAction = vi.fn(); + const account = createAccountSummary({ + accountId: "acc-paused", + displayName: "Paused Account", + status: "paused", + availableResetCredits: 1, + resetCreditNearestExpiresAt: "2026-01-03T12:00:00.000Z", + }); + + render(); + + const resetButton = screen.getByRole("button", { name: "Reset (1)" }); + expect(resetButton).toBeDisabled(); + + await user.click(resetButton); + expect(onAction).not.toHaveBeenCalledWith(account, "reset-credit"); + }); }); diff --git a/frontend/src/features/dashboard/components/account-card.tsx b/frontend/src/features/dashboard/components/account-card.tsx index 99adcb867..9e9cb19b3 100644 --- a/frontend/src/features/dashboard/components/account-card.tsx +++ b/frontend/src/features/dashboard/components/account-card.tsx @@ -113,9 +113,20 @@ export function AccountCard({ account, showAccountId = false, readOnly = false, : "No attempts"; const availableResetCredits = account.availableResetCredits ?? 0; const hasResetCredits = availableResetCredits > 0; + const resetCreditDisabled = + readOnly || status === "paused" || status === "reauth" || status === "deactivated"; const resetCountdown = account.resetCreditNearestExpiresAt ? formatSingleUnitRemaining(account.resetCreditNearestExpiresAt) : null; + const resetButtonTitle = resetCreditDisabled + ? status === "paused" + ? "Resume account to redeem reset credits" + : status === "reauth" || status === "deactivated" + ? "Re-authenticate account to redeem reset credits" + : "Reset credits unavailable" + : resetCountdown + ? `Reset (${availableResetCredits}) · ${resetCountdown.label}` + : `Reset (${availableResetCredits})`; return (
@@ -201,7 +212,8 @@ export function AccountCard({ account, showAccountId = false, readOnly = false, size="sm" variant="ghost" className="relative h-7 gap-1.5 rounded-lg pr-8 text-xs text-muted-foreground hover:text-foreground" - disabled={readOnly} + title={resetButtonTitle} + disabled={resetCreditDisabled} onClick={() => onAction?.(account, "reset-credit")} > diff --git a/frontend/src/features/dashboard/components/account-list.test.tsx b/frontend/src/features/dashboard/components/account-list.test.tsx index f3fdc9f48..a30491261 100644 --- a/frontend/src/features/dashboard/components/account-list.test.tsx +++ b/frontend/src/features/dashboard/components/account-list.test.tsx @@ -61,15 +61,18 @@ describe("AccountList", () => { render(); + const resetButton = screen.getByRole("button", { name: "Redeem reset credit for Paused Account" }); + expect(resetButton).toBeDisabled(); + await user.click(screen.getByRole("button", { name: "View details for Paused Account" })); - await user.click(screen.getByRole("button", { name: "Redeem reset credit for Paused Account" })); + await user.click(resetButton); await user.click(screen.getByRole("button", { name: "Enable limit warm-up for Paused Account" })); await user.click(screen.getByRole("button", { name: "Resume Paused Account" })); expect(onAction).toHaveBeenNthCalledWith(1, account, "details"); - expect(onAction).toHaveBeenNthCalledWith(2, account, "reset-credit"); - expect(onAction).toHaveBeenNthCalledWith(3, account, "warmup-toggle"); - expect(onAction).toHaveBeenNthCalledWith(4, account, "resume"); + expect(onAction).toHaveBeenNthCalledWith(2, account, "warmup-toggle"); + expect(onAction).toHaveBeenNthCalledWith(3, account, "resume"); + expect(onAction).not.toHaveBeenCalledWith(account, "reset-credit"); }); it("blurs list identity text when privacy mode is enabled", () => { diff --git a/frontend/src/features/dashboard/components/account-list.tsx b/frontend/src/features/dashboard/components/account-list.tsx index 7fd1f0f86..73223c094 100644 --- a/frontend/src/features/dashboard/components/account-list.tsx +++ b/frontend/src/features/dashboard/components/account-list.tsx @@ -314,9 +314,20 @@ export function AccountList({ accounts, readOnly = false, onAction }: AccountLis : "No attempts"; const availableResetCredits = account.availableResetCredits ?? 0; const hasResetCredits = availableResetCredits > 0; + const resetCreditDisabled = + readOnly || status === "paused" || status === "reauth" || status === "deactivated"; const resetCountdown = account.resetCreditNearestExpiresAt ? formatSingleUnitRemaining(account.resetCreditNearestExpiresAt) : null; + const resetButtonTitle = resetCreditDisabled + ? status === "paused" + ? "Resume account to redeem reset credits" + : status === "reauth" || status === "deactivated" + ? "Re-authenticate account to redeem reset credits" + : "Reset credits unavailable" + : resetCountdown + ? `Reset (${availableResetCredits}) · ${resetCountdown.label}` + : `Reset (${availableResetCredits})`; return (
onAction?.(account, "reset-credit")} >