diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..f7ca4f6ad78365f0169800c581ca9fc53f7f9669 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +backend/venv/ +backend/.venv/ +backend/env/ +backend/*.db +backend/.env + +# Node +frontend/node_modules/ +frontend/.next/ +frontend/.env.local +frontend/.env.production + +# Git +.git/ +.github/ + +# Docs (not needed in container) +docs/ +.kiro/ +.vscode/ +.temporary_backup/ + +# OS +.DS_Store +Thumbs.db +*.log + +# Test files +*.test.ts +*.spec.ts +__tests__/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..d9548816eac8eb2cbb489f72363ac3c9754a44a1 --- /dev/null +++ b/.env.example @@ -0,0 +1,54 @@ +# ============================================================ +# BankBot AI — Environment Configuration +# ============================================================ +# Copy this file to .env and fill in your values. +# +# FALLBACK CHAINS (no config needed for local dev): +# AI: OpenAI → Groq → Ollama → offline rule-based +# DB: PostgreSQL → SQLite (auto-fallback) +# Cache: Redis → in-memory dict (auto-fallback) +# +# You only need ONE AI key for full functionality. +# ============================================================ + +# ─── Database ──────────────────────────────────────────────── +# Leave blank to use SQLite (great for local dev / demo) +DATABASE_URL=postgresql://admin:adminpassword@localhost:5432/bankbot + +# Force SQLite regardless of DATABASE_URL +USE_SQLITE=false + +# ─── Redis Cache ───────────────────────────────────────────── +# Leave blank to use in-memory cache (auto-fallback) +REDIS_URL=redis://localhost:6379/0 + +# ─── AI Backends (Priority: OpenAI → Groq → Ollama → offline) +# Priority 1: OpenAI — fastest, most capable +# Get key: https://platform.openai.com/api-keys +OPENAI_API_KEY=sk-your-openai-key-here +OPENAI_MODEL=gpt-4o-mini + +# Priority 2: Groq — free tier, very fast inference +# Get key: https://console.groq.com/keys +GROQ_API_KEY=gsk_your-groq-key-here + +# Priority 3: Local Ollama — fully offline, no API key +# Install: https://ollama.com → then: ollama pull llama3 +OLLAMA_MODEL=llama3:latest + +# ─── Authentication ─────────────────────────────────────────── +# IMPORTANT: Change this in production! +# Generate: python -c "import secrets; print(secrets.token_hex(32))" +JWT_SECRET_KEY=bankbot-dev-secret-change-in-production +JWT_ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=60 + +# ─── CORS ──────────────────────────────────────────────────── +# JSON array of allowed frontend origins +# Production example: ["https://bankbot-ai.vercel.app"] +BACKEND_CORS_ORIGINS=["http://localhost:3000"] + +# ─── Frontend ──────────────────────────────────────────────── +# Backend API URL (no trailing slash) +# Production: https://bankbot-api.onrender.com +NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,35 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..c7493a95a80be2e4c0a3d46cda4d7fc2d0f9b4c7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: BankBot AI — CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + # ─── Backend ──────────────────────────────────────────────────────────────── + backend: + name: Backend — Lint & Import Check + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Verify all routers import cleanly + env: + USE_SQLITE: "true" + run: | + python -c " + from app.main import app + routes = [r.path for r in app.routes if hasattr(r,'path')] + print(f'Routes registered: {len(routes)}') + assert len(routes) >= 30, f'Expected 30+ routes, got {len(routes)}' + print('All routers import OK') + " + + - name: Verify demo seed script imports + env: + USE_SQLITE: "true" + run: python -c "import app.scripts.seed_demo; print('Seed script OK')" + + # ─── Frontend ─────────────────────────────────────────────────────────────── + frontend: + name: Frontend — Build & Type Check + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Type check + run: npx tsc --noEmit + + - name: Lint + run: npm run lint + + - name: Production build + env: + NEXT_PUBLIC_API_URL: http://localhost:8000 + run: npm run build + + - name: Verify build output + run: | + test -d .next/standalone && echo "Standalone build OK" || echo "No standalone (OK for non-Docker)" + test -d .next/static && echo "Static assets OK" + + # ─── Docker ───────────────────────────────────────────────────────────────── + docker: + name: Docker — Build Check + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Build backend image + run: docker build -t bankbot-backend:ci ./backend + + - name: Build frontend image + run: | + docker build \ + --build-arg NEXT_PUBLIC_API_URL=http://localhost:8000 \ + -t bankbot-frontend:ci \ + ./frontend + + - name: Smoke test backend container + run: | + docker run -d --name backend-test \ + -e USE_SQLITE=true \ + -e JWT_SECRET_KEY=ci-test-secret \ + -p 8000:8000 \ + bankbot-backend:ci + sleep 10 + curl -f http://localhost:8000/health || exit 1 + curl -f http://localhost:8000/api/status || exit 1 + echo "Backend smoke test passed" + docker stop backend-test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c55d6b6298e8210ed38fbefd91d8c8feae7cff1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +__pycache__/ +*.pyc +.streamlit/ +session.json +chat_history.json +users.json +BankBot_Accuracy_Document.docx +generate_accuracy.py +src/ +.env +.env.local +.env.production +.venv +venv/ +env/ + +# Next.js +frontend/.next/ +frontend/node_modules/ +frontend/out/ + +# Local OCR Windows Binaries (Version Agnostic) +poppler-*/ +poppler.zip +tesseract-setup.exe +tesseract-ocr/ +*.exe +*.zip +*.db diff --git a/.kiro/specs/bankbot-ai-intelligence/.config.kiro b/.kiro/specs/bankbot-ai-intelligence/.config.kiro new file mode 100644 index 0000000000000000000000000000000000000000..4c0029c8a7697caa2695a6d805fb4e5ee74e71b9 --- /dev/null +++ b/.kiro/specs/bankbot-ai-intelligence/.config.kiro @@ -0,0 +1 @@ +{"specId": "bdc55ba3-7595-4d07-b12c-ac91a1297320", "workflowType": "design-first", "specType": "feature"} diff --git a/.kiro/specs/bankbot-ai-intelligence/design.md b/.kiro/specs/bankbot-ai-intelligence/design.md new file mode 100644 index 0000000000000000000000000000000000000000..16fa0ad2afefc8bbe89496a04177249c4828d21f --- /dev/null +++ b/.kiro/specs/bankbot-ai-intelligence/design.md @@ -0,0 +1,1393 @@ +# Design Document: BankBot AI Intelligence & API (Phase 4) + +## Overview + +Phase 4 delivers the complete AI intelligence layer for BankBot — a FastAPI-based backend that +exposes financial forecasting, behavioral analytics, coaching, fraud detection, simulation, and +real-time WebSocket chat. The system is built for resilience: it auto-detects and falls back +across OpenAI → Groq → local Ollama for AI, PostgreSQL → SQLite for persistence, and +Redis → in-memory TTL cache for caching, so the application runs immediately in any environment. + +The AI layer is already partially implemented. This design documents the full intended architecture, +the contracts between modules, the formal specifications for key algorithms, and the integration +points that must be verified or completed. + +--- + +## Architecture + +```mermaid +graph TD + FE["Next.js Frontend\n(port 3000)"] + GW["FastAPI Gateway\n(main.py — port 8000)"] + AIR["ai/router.py\n(HTTP endpoints)"] + WSR["websocket/router.py\n(WS endpoint)"] + CM["websocket/connection_manager.py"] + + subgraph AI_Engines["AI Engine Modules (backend/app/ai/)"] + FC["forecasting.py"] + SIM["simulation.py"] + BEH["behavior.py"] + COA["coaching.py"] + SUB["subscriptions.py"] + FRD["fraud.py"] + CHT["chat.py"] + OLL["ollama_integration.py"] + end + + subgraph Infra["Infrastructure Layer"] + DB["database/database.py\n(PostgreSQL → SQLite fallback)"] + MDL["database/models.py\n(SQLAlchemy ORM)"] + CAC["middleware/cache.py\n(Redis → MemoryCache fallback)"] + end + + subgraph AI_Backends["External AI Backends"] + OAI["OpenAI API\n(gpt-4o-mini)"] + GRQ["Groq API\n(llama-3.3-70b)"] + LOC["Local Ollama\n(llama3:latest)"] + end + + FE -->|"REST /api/ai/*"| GW + FE -->|"WS /api/ai/chat/ws"| GW + GW --> AIR + GW --> WSR + WSR --> CM + AIR --> FC & SIM & BEH & COA & SUB & FRD & CHT + WSR --> CHT + CHT --> OLL + COA --> OLL + OLL -->|"priority 1"| OAI + OLL -->|"priority 2"| GRQ + OLL -->|"priority 3"| LOC + AIR --> CAC + FC & SIM & BEH & COA & SUB & FRD & CHT --> DB + DB --> MDL +``` + +--- + +## Sequence Diagrams + +### HTTP AI Endpoint Request Flow + +```mermaid +sequenceDiagram + participant C as Client + participant R as ai/router.py + participant CAC as cache.py + participant ENG as AI Engine Module + participant DB as database.py + participant LLM as AI Backend (OpenAI/Groq/Ollama) + + C->>R: GET /api/ai/twin/predict?user_id=X + R->>CAC: cache.get("ai:twin:predict:X") + alt Cache Hit + CAC-->>R: cached JSON + R-->>C: 200 OK (cached) + else Cache Miss + R->>ENG: predict_future_balance(db, user_id) + ENG->>DB: query Account, Transaction + DB-->>ENG: ORM objects + ENG-->>R: result dict + R->>CAC: cache.set(key, result, ttl=300) + R-->>C: 200 OK (fresh) + end +``` + +### WebSocket Streaming Chat Flow + +```mermaid +sequenceDiagram + participant C as Browser Client + participant WS as websocket/router.py + participant CM as connection_manager.py + participant CHT as chat.py + participant LLM as AI Backend + + C->>WS: WS connect /api/ai/chat/ws?user_id=X + WS->>CM: ws_manager.connect(websocket, user_id) + CM-->>WS: accepted + C->>WS: send JSON {"type":"chat","message":"..."} + WS->>CHT: stream_chat_response(db, user_id, prompt) + CHT->>LLM: streaming completion request + loop For each token chunk + LLM-->>CHT: token + CHT-->>WS: yield chunk + WS-->>C: send_json {"type":"chat_chunk","content":"..."} + end + WS-->>C: send_json {"type":"chat_end"} +``` + +### AI Backend Fallback Chain + +```mermaid +sequenceDiagram + participant M as Module (chat/coaching) + participant OLL as ollama_integration.py + participant OAI as OpenAI API + participant GRQ as Groq API + participant LOC as Local Ollama + + M->>OLL: get_ai_response(prompt) + alt OPENAI_API_KEY set + OLL->>OAI: chat.completions.create(gpt-4o-mini) + OAI-->>OLL: response + else GROQ_API_KEY set + OLL->>GRQ: client.chat.completions.create + GRQ-->>OLL: response + else Ollama reachable + OLL->>LOC: POST /api/chat (llama3:latest) + LOC-->>OLL: response + else All backends down + OLL-->>M: None + M->>M: get_offline_chat_fallback() + end + OLL-->>M: response string +``` + +--- + +## Components and Interfaces + +### Component 1: database/database.py — Resilient DB Engine + +**Purpose**: Provides a SQLAlchemy engine and session factory with automatic PostgreSQL → SQLite fallback. + +**Interface**: +```python +engine: Engine # SQLAlchemy engine (PostgreSQL or SQLite) +SessionLocal: sessionmaker # Session factory +Base: DeclarativeMeta # ORM base class + +def get_db() -> Generator[Session, None, None]: + """FastAPI dependency — yields a DB session, closes on exit.""" +``` + +**Responsibilities**: +- Read `DATABASE_URL` from env; attempt PostgreSQL connection +- On `OperationalError`, switch to `sqlite:///./bankbot.db` with `check_same_thread=False` +- Expose `get_db()` as a FastAPI `Depends` injectable + +--- + +### Component 2: middleware/cache.py — Resilient Cache + +**Purpose**: Provides a unified `get/set/delete` cache interface backed by Redis or an in-memory TTL dict. + +**Interface**: +```python +class MemoryCache: + def get(self, key: str) -> Any | None + def set(self, key: str, value: Any, ttl: int | None = None) -> None + def delete(self, key: str) -> None + +class CacheManager: + def get(self, key: str) -> Any | None + def set(self, key: str, value: Any, ttl: int | None = None) -> None + def delete(self, key: str) -> None + +cache: CacheManager # module-level singleton +``` + +**Responsibilities**: +- On startup, attempt `redis.Redis.from_url(...).ping()` +- On failure, fall back to `MemoryCache` (thread-safe via `threading.Lock`) +- Serialize/deserialize values as JSON when using Redis + +--- + +### Component 3: ai/forecasting.py — Financial Twin Engine + +**Purpose**: Computes balance projections, savings/investment growth curves, and scenario comparisons. + +**Interface**: +```python +def get_cashflow_metrics(db: Session, user_id: str, days: int = 90 + ) -> tuple[float, float, float]: + """Returns (current_balance, avg_daily_income, avg_daily_spending).""" + +def predict_future_balance(db: Session, user_id: str, projection_days: int = 90 + ) -> dict: + """Returns chart-ready balance projection for 30/60/90 days.""" + +def forecast_savings_and_investments(db: Session, user_id: str, projection_months: int = 12 + ) -> dict: + """Returns monthly savings growth, investment growth, and debt decline curves.""" + +def simulate_future_scenarios(db: Session, user_id: str, projection_months: int = 6 + ) -> dict: + """Returns three scenario trajectories: status_quo, frugal, lifestyle_inflation.""" +``` + +--- + +### Component 4: ai/simulation.py — What-If Simulator + +**Purpose**: Evaluates the financial impact of hypothetical purchases, investment changes, and subscription cancellations. + +**Interface**: +```python +def simulate_purchase_impact( + db: Session, user_id: str, amount: float, category: str, merchant: str +) -> dict: + """Returns risk_level, projected_balance, emergency_buffer_breached, recommendation.""" + +def simulate_investment_impact( + db: Session, user_id: str, monthly_sip: float, asset_type: str, lump_sum: float = 0.0 +) -> dict: + """Returns 1/3/5-year growth projection, affordability check, risk_level.""" + +def simulate_subscription_cancellation( + db: Session, user_id: str, subscription_ids: list[str] +) -> dict: + """Returns monthly/yearly savings, goal impact, and recommendation.""" +``` + +--- + +### Component 5: ai/behavior.py — Behavioral Analytics Engine + +**Purpose**: Detects spending patterns (late-night, weekend spikes, dopamine/stress triggers) from transaction history. + +**Interface**: +```python +def analyze_spending_behavior(db: Session, user_id: str, days: int = 90) -> dict: + """ + Returns: + insights: list[str] — human-readable behavioral findings + metrics: dict — late_night_count, weekend_pct, impulsive_count, etc. + category_breakdown: dict — spending totals per category + """ +``` + +--- + +### Component 6: ai/coaching.py — Financial Health Coach + +**Purpose**: Computes a multi-dimensional Financial Health Score and generates LLM-grounded daily briefings. + +**Interface**: +```python +def calculate_financial_health_score(db: Session, user_id: str) -> dict: + """ + Returns overall_score (0-100), per-category sub-scores, LLM explanation, + and actionable_improvements list. + """ + +def generate_daily_briefing(db: Session, user_id: str) -> dict: + """ + Returns date, user_name, LLM-generated briefing text, and key metrics dict. + """ +``` + +--- + +### Component 7: ai/subscriptions.py — Subscription Optimizer + +**Purpose**: Detects duplicate, unused, and cancellable subscriptions from the `subscriptions` table. + +**Interface**: +```python +def analyze_subscriptions(db: Session, user_id: str) -> dict: + """ + Returns subscriptions list, duplicates list, unused_subscriptions list, + yearly_savings_potential, and risk_analysis per merchant. + """ +``` + +--- + +### Component 8: ai/fraud.py — Fraud & Anomaly Detector + +**Purpose**: Scores individual transactions for fraud risk using rule-based heuristics and logs alerts. + +**Interface**: +```python +def evaluate_transaction_for_fraud(db: Session, transaction_id: str) -> dict: + """ + Returns fraud_risk_score (0-100), is_anomalous, explanations list, status string. + Writes FraudLog to DB if score >= 30. + """ + +def get_user_fraud_alerts(db: Session, user_id: str) -> dict: + """Returns total_alerts, pending_reviews count, and full alerts list.""" +``` + +--- + +### Component 9: ai/chat.py — Contextual Chat Agent + +**Purpose**: Provides HTTP and streaming chat responses grounded in the user's live financial profile, with session memory. + +**Interface**: +```python +class ChatMemoryManager: + def get_history(self, user_id: str) -> list[dict] + def add_message(self, user_id: str, role: str, content: str) -> None + def clear_history(self, user_id: str) -> None + +def build_user_context_string(db: Session, user_id: str) -> str: + """Assembles a financial profile string from DB for the system prompt.""" + +def get_contextual_system_prompt(db: Session, user_id: str) -> str: + """Returns the full system prompt with embedded financial context.""" + +def get_chat_response(db: Session, user_id: str, prompt: str) -> str: + """Synchronous HTTP chat — tries OpenAI → Groq → Ollama → offline fallback.""" + +def stream_chat_response(db: Session, user_id: str, prompt: str) -> Generator[str, None, None]: + """Streaming generator — yields token chunks for WebSocket delivery.""" +``` + +--- + +### Component 10: ai/ollama_integration.py — AI Backend Abstraction + +**Purpose**: Abstracts OpenAI, Groq, and local Ollama behind a unified interface; detects available backends at startup. + +**Interface**: +```python +def has_active_ai_backend() -> bool +def get_ai_response(prompt: str, history: list | None, language: str) -> str | None +def stream_ai_response(prompt: str, history: list | None, language: str) -> Generator[str, None, None] +def get_groq_response(prompt, history, model, language) -> str | None +def stream_groq_response(prompt, history, model, language) -> Generator[str, None, None] +def get_ollama_response(prompt, history, model, language) -> str | None +def stream_ollama_response(prompt, history, model, language) -> Generator[str, None, None] +``` + +--- + +### Component 11: websocket/connection_manager.py — WebSocket Registry + +**Purpose**: Maintains a per-user registry of active WebSocket connections; supports targeted and broadcast messaging. + +**Interface**: +```python +class WebSocketConnectionManager: + async def connect(self, websocket: WebSocket, user_id: str) -> None + def disconnect(self, websocket: WebSocket, user_id: str) -> None + async def send_personal_message(self, message: dict, user_id: str) -> None + async def broadcast(self, message: dict) -> None + +ws_manager: WebSocketConnectionManager # module-level singleton +``` + +--- + +### Component 12: ai/router.py — HTTP API Router + +**Purpose**: Mounts all `/api/ai/*` HTTP endpoints, applies cache-aside pattern, and delegates to engine modules. + +**Endpoints**: + +| Method | Path | Engine Function | Cache TTL | +|--------|------|-----------------|-----------| +| GET | `/api/ai/twin/predict` | `predict_future_balance` | 300s | +| GET | `/api/ai/twin/future` | `forecast_savings_and_investments` | 300s | +| GET | `/api/ai/twin/scenarios` | `simulate_future_scenarios` | 300s | +| POST | `/api/ai/simulate/purchase` | `simulate_purchase_impact` | none | +| POST | `/api/ai/simulate/investment` | `simulate_investment_impact` | none | +| POST | `/api/ai/simulate/subscription` | `simulate_subscription_cancellation` | none | +| GET | `/api/ai/behavior/insights` | `analyze_spending_behavior` | 600s | +| GET | `/api/ai/coaching/briefing` | `generate_daily_briefing` | 3600s | +| GET | `/api/ai/coaching/score` | `calculate_financial_health_score` | 600s | +| GET | `/api/ai/subscriptions/optimize` | `analyze_subscriptions` | 600s | +| GET | `/api/ai/fraud/analysis` | `get_user_fraud_alerts` | none | +| POST | `/api/ai/fraud/evaluate/{id}` | `evaluate_transaction_for_fraud` | none | +| POST | `/api/ai/chat` | `get_chat_response` | none | + +--- + +### Component 13: websocket/router.py — WebSocket Endpoint + +**Purpose**: Handles the `/api/ai/chat/ws` WebSocket lifecycle, dispatches message types, and streams AI replies. + +**Message Protocol**: + +| Direction | JSON Shape | Meaning | +|-----------|-----------|---------| +| Client → Server | `{"type":"chat","message":"..."}` | Send a chat prompt | +| Client → Server | `{"type":"ping"}` | Keepalive | +| Server → Client | `{"type":"chat_start"}` | AI generation beginning | +| Server → Client | `{"type":"chat_chunk","content":"..."}` | Streaming token | +| Server → Client | `{"type":"chat_end"}` | Generation complete | +| Server → Client | `{"type":"pong"}` | Keepalive reply | +| Server → Client | `{"type":"error","message":"..."}` | Error notification | + +--- + +## Data Models + +### Core ORM Models (database/models.py) + +```python +class User(Base): + id: str (UUID PK) + email: str (unique) + password_hash: str + profile_data: JSON # {"name": str, "phone": str} + financial_personality: str # "Saver" | "Investor" | "Impulsive Spender" | ... + ai_personalization_settings: JSON + created_at: DateTime + # relationships: accounts, subscriptions, goals, investments, ai_insights, notifications + +class Account(Base): + id: str (UUID PK) + user_id: str (FK → users.id) + type: str # "checking" | "savings" + balance: float + currency: str # default "USD" + status: str # "active" | "inactive" + # relationships: transactions + +class Transaction(Base): + id: str (UUID PK) + account_id: str (FK → accounts.id) + amount: float + type: str # "credit" | "debit" + category: str # "Food" | "Shopping" | "Income" | ... + timestamp: DateTime + merchant: str + tags: JSON # list[str] + ai_generated_metadata: JSON # {"is_recurring": bool, "confidence": float} + spending_emotion_label: str # "happy" | "regret" | "neutral" | "essential" + +class Subscription(Base): + id: str (UUID PK) + user_id: str (FK → users.id) + merchant: str + amount: float + billing_cycle: str # "monthly" | "yearly" + active: bool + ai_usage_detection: JSON # {"usage_frequency": "high"|"medium"|"low"|"none"} + +class Goal(Base): + id: str (UUID PK) + user_id: str (FK → users.id) + title: str + target_amount: float + current_amount: float + target_date: DateTime + ai_generated_plan: JSON # {"monthly_saving_required": float, "risk": str} + +class Investment(Base): + id: str (UUID PK) + user_id: str (FK → users.id) + asset_name: str + type: str # "stock" | "crypto" | "mutual_fund" | "fd" | "bond" + amount_invested: float + current_value: float + portfolio_allocation: float # percentage + ai_risk_analysis: JSON # {"risk_level": str, "recommendation": str} + +class FraudLog(Base): + id: str (UUID PK) + transaction_id: str (FK → transactions.id, unique) + risk_score: float # 0.0 – 1.0 + suspicious_activity_details: str + status: str # "pending" | "resolved" | "false_positive" +``` + +### API Response Schemas (Pydantic — ai/router.py) + +```python +class PurchaseRequest(BaseModel): + amount: float + merchant: str + category: str + +class InvestmentRequest(BaseModel): + monthly_sip: float + asset_type: str # "stock" | "crypto" | "mutual_fund" | "fd" | "bond" + lump_sum: float = 0.0 + +class SubscriptionSimulationRequest(BaseModel): + subscription_ids: list[str] + +class ChatMessageRequest(BaseModel): + message: str +``` + +--- + +## Algorithmic Pseudocode + +### Algorithm 1: Balance Projection (predict_future_balance) + +```pascal +ALGORITHM predict_future_balance(db, user_id, projection_days=90) +INPUT: db — SQLAlchemy Session + user_id — string UUID + projection_days — integer in [1, 365] +OUTPUT: result — dict with chart_data, projected_balance, insight + +BEGIN + // Step 1: Gather cashflow metrics + (current_balance, daily_income, daily_spending) ← get_cashflow_metrics(db, user_id, days=90) + + // Step 2: Compute net daily cashflow + net_daily ← daily_income - daily_spending + + // Step 3: Project terminal balance + projected_balance ← MAX(0.0, current_balance + net_daily * projection_days) + + // Step 4: Compute percentage change + IF current_balance > 0 THEN + percent_change ← (projected_balance - current_balance) / current_balance * 100 + ELSE + percent_change ← 0.0 + END IF + + // Step 5: Build chart data (every 5 days) + chart_data ← [] + FOR day ← 0 TO projection_days STEP 5 DO + ASSERT day >= 0 AND day <= projection_days + val ← MAX(0.0, current_balance + net_daily * day) + chart_data.APPEND({date: now() + day_offset(day), balance: ROUND(val, 2)}) + END FOR + + ASSERT LENGTH(chart_data) >= 1 + ASSERT chart_data[0].balance = ROUND(current_balance, 2) + + RETURN {current_balance, projected_balance, percent_change, net_daily, insight, chart_data} +END +``` + +**Preconditions:** +- `user_id` references an existing user with at least one account +- `projection_days` is a positive integer + +**Postconditions:** +- `projected_balance >= 0.0` (floored at zero, no negative balances) +- `chart_data[0].balance == current_balance` (first point is always current state) +- `len(chart_data) == ceil(projection_days / 5) + 1` + +**Loop Invariant:** For each iteration, `day` is a non-negative multiple of 5 and `val >= 0.0` + +--- + +### Algorithm 2: Financial Health Score (calculate_financial_health_score) + +```pascal +ALGORITHM calculate_financial_health_score(db, user_id) +INPUT: db — SQLAlchemy Session + user_id — string UUID +OUTPUT: result — dict with overall_score (0-100), categories, explanation, improvements + +BEGIN + // Gather raw data + accounts ← db.query(Account).filter(user_id) + total_balance ← SUM(acc.balance FOR acc IN accounts) + savings_balance ← SUM(acc.balance FOR acc IN accounts WHERE acc.type = "savings") + (_, daily_income, daily_spending) ← get_cashflow_metrics(db, user_id) + monthly_income ← MAX(1000.0, daily_income * 30.4) + monthly_spending ← daily_spending * 30.4 + + // Sub-score 1: Savings Consistency (max 20) + goal_savings ← SUM(g.current_amount FOR g IN goals) + IF goal_savings > 1000 THEN savings_score ← 20 + ELSE IF goal_savings > 0 THEN savings_score ← 15 + ELSE savings_score ← 10 + END IF + + // Sub-score 2: Debt Ratio (max 20) + debt_goals ← SUM(g.target - g.current FOR g IN goals WHERE "debt" IN g.title) + debt_to_income ← (debt_goals * 0.05) / monthly_income + IF debt_to_income > 0.40 THEN debt_score ← 5 + ELSE IF debt_to_income > 0.20 THEN debt_score ← 12 + ELSE IF debt_to_income > 0.05 THEN debt_score ← 18 + ELSE debt_score ← 20 + END IF + + // Sub-score 3: Spending Discipline (max 20) + savings_rate ← (monthly_income - monthly_spending) / monthly_income + IF savings_rate >= 0.30 THEN discipline_score ← 20 + ELSE IF savings_rate >= 0.15 THEN discipline_score ← 16 + ELSE IF savings_rate >= 0.0 THEN discipline_score ← 12 + ELSE discipline_score ← 5 + END IF + + // Sub-score 4: Emergency Fund (max 20) + months_buffer ← savings_balance / MAX(500.0, monthly_spending) + IF months_buffer >= 6.0 THEN emergency_score ← 20 + ELSE IF months_buffer >= 3.0 THEN emergency_score ← 15 + ELSE IF months_buffer >= 1.0 THEN emergency_score ← 8 + ELSE emergency_score ← 0 + END IF + + // Sub-score 5: Investment Index (max 10) + inv_total ← SUM(i.current_value FOR i IN investments) + IF inv_total > 5000 THEN investment_score ← 10 + ELSE IF inv_total > 0 THEN investment_score ← 6 + ELSE investment_score ← 0 + END IF + + // Sub-score 6: Subscription Efficiency (max 10) + sub_cost ← SUM(monthly_cost(s) FOR s IN active_subscriptions) + sub_ratio ← sub_cost / monthly_income + IF sub_ratio > 0.10 THEN sub_score ← 3 + ELSE IF sub_ratio > 0.05 THEN sub_score ← 7 + ELSE sub_score ← 10 + END IF + + // Aggregate + overall_score ← CLAMP(savings_score + debt_score + discipline_score + + emergency_score + investment_score + sub_score, 0, 100) + + ASSERT overall_score >= 0 AND overall_score <= 100 + ASSERT savings_score + debt_score + discipline_score + emergency_score + + investment_score + sub_score = overall_score (before clamping) + + RETURN {overall_score, categories, explanation (LLM or fallback), improvements} +END +``` + +**Preconditions:** +- `monthly_income` is floored at 1000.0 to prevent division-by-zero +- `monthly_spending` is floored at 500.0 for emergency fund calculation + +**Postconditions:** +- `0 <= overall_score <= 100` +- All six sub-scores are non-negative and within their declared maximums +- `improvements` is non-empty (at least one suggestion or "maintain habits" message) + +--- + +### Algorithm 3: Fraud Risk Scoring (evaluate_transaction_for_fraud) + +```pascal +ALGORITHM evaluate_transaction_for_fraud(db, transaction_id) +INPUT: db — SQLAlchemy Session + transaction_id — string UUID +OUTPUT: result — dict with fraud_risk_score, is_anomalous, explanations, status + +BEGIN + txn ← db.query(Transaction).filter(id = transaction_id).first() + IF txn IS NULL THEN RETURN {error: "Transaction not found"} END IF + + account ← db.query(Account).filter(id = txn.account_id).first() + history ← last 30 debit transactions for account.user_id (excluding txn) + + score ← 0 + reasons ← [] + + // Rule 1: Amount spike detection + IF history IS NOT EMPTY THEN + avg_amount ← MEAN(h.amount FOR h IN history) + std_amount ← STDDEV(h.amount FOR h IN history) + IF txn.amount > avg_amount * 3.5 THEN + score ← score + 40 + reasons.APPEND("Amount is 3.5x historical average") + ELSE IF txn.amount > avg_amount * 2.0 THEN + score ← score + 20 + reasons.APPEND("Amount is 2x historical average") + END IF + END IF + + // Rule 2: Late-night timing (11PM – 4AM) + hour ← txn.timestamp.hour + IF hour >= 23 OR hour < 4 THEN + score ← score + 25 + reasons.APPEND("Unusual timing: 11PM–4AM") + END IF + + // Rule 3: High-frequency (< 3 minutes since last transaction) + IF history IS NOT EMPTY THEN + time_diff ← ABS(txn.timestamp - history[0].timestamp).seconds + IF time_diff < 180 THEN + score ← score + 20 + reasons.APPEND("Multiple transactions within 3 minutes") + END IF + END IF + + // Rule 4: Duplicate detection (same merchant + amount within 10 minutes) + FOR prev IN history[0..4] DO + time_diff ← ABS(txn.timestamp - prev.timestamp).seconds + IF prev.merchant = txn.merchant AND prev.amount = txn.amount AND time_diff < 600 THEN + score ← score + 30 + reasons.APPEND("Potential duplicate payment") + BREAK + END IF + END FOR + + score ← MIN(100, score) + + // Persist if above threshold + IF score >= 30 AND NOT EXISTS FraudLog(transaction_id) THEN + db.INSERT FraudLog(transaction_id, risk_score=score/100, details=reasons, status="pending") + db.COMMIT() + END IF + + status ← IF score >= 50 THEN "flagged" + ELSE IF score >= 30 THEN "suspicious" + ELSE "verified" + + ASSERT score >= 0 AND score <= 100 + RETURN {transaction_id, fraud_risk_score: score, is_anomalous: score >= 30, explanations: reasons, status} +END +``` + +**Preconditions:** +- `transaction_id` must reference an existing transaction with a valid account +- History window is capped at 30 transactions to bound computation + +**Postconditions:** +- `0 <= fraud_risk_score <= 100` +- A `FraudLog` row is created if and only if `score >= 30` and no prior log exists +- `status` is exactly one of `"flagged"`, `"suspicious"`, or `"verified"` + +**Loop Invariant (duplicate check):** At each iteration, all previously checked transactions were not duplicates + +--- + +### Algorithm 4: Behavioral Pattern Detection (analyze_spending_behavior) + +```pascal +ALGORITHM analyze_spending_behavior(db, user_id, days=90) +INPUT: db — SQLAlchemy Session + user_id — string UUID + days — lookback window in days +OUTPUT: result — dict with insights, metrics, category_breakdown + +BEGIN + account_ids ← [acc.id FOR acc IN accounts WHERE user_id] + txns ← debit transactions in last `days` days for account_ids + + IF txns IS EMPTY THEN RETURN default_empty_result END IF + + amounts ← [t.amount FOR t IN txns] + avg_txn ← MEAN(amounts) + std_txn ← STDDEV(amounts) + + // Classify each transaction + FOR t IN txns DO + hour ← t.timestamp.hour + day_of_week ← t.timestamp.weekday() + + IF hour >= 23 OR hour < 4 THEN late_night_txns.ADD(t) END IF + IF day_of_week IN {4, 5, 6} THEN weekend_txns.ADD(t) END IF + + IF t.amount > (avg_txn + 1.5 * std_txn) AND t.category IN {"Shopping","Entertainment","Food"} THEN + impulsive_txns.ADD(t) + END IF + + emotion ← LOWER(t.spending_emotion_label OR "") + IF emotion = "regret" THEN stress_txns.ADD(t) + ELSE IF emotion IN {"happy","dopamine"} OR (t.category = "Shopping" AND t.amount > avg_txn) THEN + dopamine_txns.ADD(t) + END IF + + category_totals[t.category OR "Other"] += t.amount + END FOR + + // Generate insights + insights ← [] + late_night_pct ← len(late_night_txns) / len(txns) * 100 + IF late_night_pct > 15 THEN insights.ADD(late_night_warning) END IF + + weekend_pct ← len(weekend_txns) / len(txns) * 100 + IF weekend_pct > 45 AND weekend_avg > weekday_avg * 1.2 THEN + insights.ADD(weekend_spike_warning) + END IF + + IF len(dopamine_txns) > 3 THEN insights.ADD(dopamine_warning) END IF + IF len(stress_txns) > 0 THEN insights.ADD(stress_warning) END IF + IF insights IS EMPTY THEN insights.ADD(stable_spending_message) END IF + + ASSERT len(insights) >= 1 + RETURN {insights, metrics, category_breakdown} +END +``` + +**Preconditions:** +- Only `debit` transactions are analyzed (income credits are excluded) +- `std_txn` defaults to 0.0 when fewer than 2 transactions exist + +**Postconditions:** +- `insights` always contains at least one entry +- `metrics.weekend_pct` is in range `[0.0, 100.0]` +- `category_breakdown` values are non-negative floats + +--- + +### Algorithm 5: Cache-Aside Pattern (ai/router.py) + +```pascal +ALGORITHM cache_aside_get(cache_key, ttl, compute_fn, db, user_id) +INPUT: cache_key — string + ttl — integer seconds + compute_fn — function(db, user_id) → dict + db — Session + user_id — string +OUTPUT: result — dict (from cache or freshly computed) + +BEGIN + cached ← cache.get(cache_key) + IF cached IS NOT NULL THEN + RETURN cached + END IF + + result ← compute_fn(db, user_id) + cache.set(cache_key, result, ttl=ttl) + RETURN result +END +``` + +**Preconditions:** +- `compute_fn` is a pure function with no side effects on the DB +- `ttl > 0` + +**Postconditions:** +- Returned value is semantically equivalent whether served from cache or computed fresh +- Cache is populated after a miss so the next call within TTL is served from cache + +--- + +### Algorithm 6: Compound Growth Projection (forecast_savings_and_investments) + +```pascal +ALGORITHM forecast_savings_and_investments(db, user_id, projection_months=12) +INPUT: db — Session, user_id — string, projection_months — int +OUTPUT: dict with savings_growth, investment_growth, debt_decline arrays + +BEGIN + savings_apr ← 0.04 // 4% APY + investment_apr ← 0.08 // 8% APY + + (_, daily_income, daily_spending) ← get_cashflow_metrics(db, user_id) + net_monthly ← MAX(0.0, (daily_income - daily_spending) * 30.4) + monthly_savings_addition ← net_monthly * 0.5 + monthly_investment_addition ← net_monthly * 0.3 + monthly_debt_payment ← MAX(150.0, net_monthly * 0.1) + + current_savings ← savings_balance + current_inv ← total_invested + total_debt ← debt_from_goals OR 5000.0 + + FOR month ← 0 TO projection_months DO + IF month > 0 THEN + // Compound interest with monthly additions + current_savings ← (current_savings + monthly_savings_addition) * (1 + savings_apr / 12) + current_inv ← (current_inv + monthly_investment_addition) * (1 + investment_apr / 12) + total_debt ← MAX(0.0, total_debt - monthly_debt_payment) + END IF + + ASSERT current_savings >= 0.0 + ASSERT current_inv >= 0.0 + ASSERT total_debt >= 0.0 + + savings_data.APPEND({month: "Month N", amount: ROUND(current_savings, 2)}) + investment_data.APPEND({month: "Month N", amount: ROUND(current_inv, 2)}) + debt_data.APPEND({month: "Month N", amount: ROUND(total_debt, 2)}) + END FOR + + RETURN {savings_growth, investment_growth, debt_decline, ...summary_fields} +END +``` + +**Loop Invariant:** At each iteration, `current_savings >= 0`, `current_inv >= 0`, `total_debt >= 0` + +**Postconditions:** +- All three arrays have exactly `projection_months + 1` entries +- `debt_decline` is monotonically non-increasing +- `savings_growth` and `investment_growth` are monotonically non-decreasing (given positive net monthly) + +--- + +## Key Functions with Formal Specifications + +### get_cashflow_metrics + +```python +def get_cashflow_metrics(db: Session, user_id: str, days: int = 90 + ) -> tuple[float, float, float]: +``` + +**Preconditions:** +- `days > 0` +- `user_id` is a valid string (may reference a user with no accounts) + +**Postconditions:** +- Returns `(current_balance, avg_daily_income, avg_daily_spending)` — all `>= 0.0` +- If no accounts exist: returns `(0.0, 0.0, 0.0)` +- If no transactions in window: returns `(current_balance, 0.0, 0.0)` +- `avg_daily_income = total_credits_in_window / days` +- `avg_daily_spending = total_debits_in_window / days` + +--- + +### simulate_purchase_impact + +```python +def simulate_purchase_impact( + db: Session, user_id: str, amount: float, category: str, merchant: str +) -> dict: +``` + +**Preconditions:** +- `amount > 0.0` +- `category` and `merchant` are non-empty strings + +**Postconditions:** +- `projected_balance = MAX(0.0, total_balance - amount)` +- `risk_level` ∈ `{"low", "medium", "high", "critical"}` +- `emergency_buffer_breached = (total_balance - amount) < emergency_threshold` +- `recommendation` is a non-empty string + +--- + +### simulate_investment_impact + +```python +def simulate_investment_impact( + db: Session, user_id: str, monthly_sip: float, asset_type: str, lump_sum: float = 0.0 +) -> dict: +``` + +**Preconditions:** +- `monthly_sip >= 0.0` +- `lump_sum >= 0.0` +- `asset_type` ∈ `{"stock", "crypto", "mutual_fund", "fd", "bond"}` (defaults to 7% APR for unknown) + +**Postconditions:** +- `growth_projection` contains exactly 3 entries (year 1, 3, 5) +- For each entry: `future_value >= total_invested` (compound growth is non-negative) +- `is_affordable = (monthly_net >= monthly_sip)` + +--- + +### stream_chat_response + +```python +def stream_chat_response(db: Session, user_id: str, prompt: str + ) -> Generator[str, None, None]: +``` + +**Preconditions:** +- `prompt` is a non-empty string +- `user_id` references an existing user (or fallback user is used) + +**Postconditions:** +- Yields at least one non-empty string chunk +- After all chunks are yielded, `chat_memory` contains the user message and assembled assistant reply +- History is capped at 12 messages (6 conversation rounds) +- Never raises an exception to the caller — all backend errors are caught and a fallback chunk is yielded + +--- + +### WebSocketConnectionManager.connect + +```python +async def connect(self, websocket: WebSocket, user_id: str) -> None: +``` + +**Preconditions:** +- `websocket` is an unaccepted FastAPI WebSocket instance +- `user_id` is a non-empty string + +**Postconditions:** +- `websocket.accept()` has been called +- `user_id` key exists in `self.active_connections` +- `websocket` is present in `self.active_connections[user_id]` +- Multiple connections per user are supported (list, not single slot) + +--- + +## Example Usage + +### 1. Balance Prediction (HTTP) + +```python +import httpx + +# Get 90-day balance projection for the first user in DB +response = httpx.get("http://localhost:8000/api/ai/twin/predict") +data = response.json() + +# Expected shape: +# { +# "current_balance": 12500.00, +# "projected_balance": 14200.00, +# "percent_change": 13.6, +# "net_daily": 18.89, +# "insight": "Based on current trends, your total balance is projected to grow...", +# "chart_data": [{"date": "2025-05-24", "balance": 12500.00}, ...] +# } +print(data["insight"]) +``` + +### 2. Purchase Simulation (HTTP POST) + +```python +response = httpx.post( + "http://localhost:8000/api/ai/simulate/purchase", + json={"amount": 3500.0, "merchant": "Tesla Dealership", "category": "Transport"} +) +data = response.json() + +# Expected shape: +# { +# "risk_analysis": {"risk_level": "high", "reasons": [...]}, +# "projected_balance": 9000.00, +# "recommendation": "⚠️ Refrain from this purchase if possible..." +# } +``` + +### 3. Financial Health Score (HTTP) + +```python +response = httpx.get("http://localhost:8000/api/ai/coaching/score") +data = response.json() + +# Expected shape: +# { +# "overall_score": 72.0, +# "categories": { +# "savings_consistency": {"score": 15.0, "max": 20}, +# "debt_ratio": {"score": 18.0, "max": 20}, +# ... +# }, +# "explanation": "As a Saver, your financial health score of 72 reflects...", +# "actionable_improvements": ["Build savings buffer...", ...] +# } +``` + +### 4. WebSocket Streaming Chat + +```python +import asyncio +import websockets +import json + +async def chat(): + uri = "ws://localhost:8000/api/ai/chat/ws" + async with websockets.connect(uri) as ws: + await ws.send(json.dumps({"type": "chat", "message": "What is my savings rate?"})) + + full_reply = "" + async for raw in ws: + msg = json.loads(raw) + if msg["type"] == "chat_chunk": + full_reply += msg["content"] + print(msg["content"], end="", flush=True) + elif msg["type"] == "chat_end": + break + +asyncio.run(chat()) +``` + +### 5. Fraud Evaluation (HTTP POST) + +```python +# Evaluate a specific transaction +response = httpx.post( + "http://localhost:8000/api/ai/fraud/evaluate/some-transaction-uuid" +) +data = response.json() + +# Expected shape: +# { +# "fraud_risk_score": 65, +# "is_anomalous": true, +# "status": "flagged", +# "explanations": ["Amount is 3.5x historical average", "Unusual timing: 11PM–4AM"] +# } +``` + +### 6. Subscription Optimization (HTTP) + +```python +response = httpx.get("http://localhost:8000/api/ai/subscriptions/optimize") +data = response.json() + +# Expected shape: +# { +# "subscriptions": [...], +# "duplicates": [{"merchant": "Netflix", "count": 2, ...}], +# "unused_subscriptions": [{"merchant": "Gym", "yearly_savings": 240.0, ...}], +# "yearly_savings_potential": 240.0, +# "risk_analysis": [...] +# } +``` + +--- + +## Correctness Properties + +The following properties must hold universally across all valid inputs: + +Property 1: Balance non-negativity — For all users and projection windows, +`projected_balance >= 0.0`. Balance is floored at zero; net negative cashflow +cannot produce a negative projected balance. + +**Validates: Requirements 1.1** + +Property 2: Score boundedness — For all users, +`0.0 <= overall_financial_health_score <= 100.0`. The score is clamped after +summing all six sub-scores. + +**Validates: Requirements 1.2** + +Property 3: Score sub-score consistency — The sum of all six sub-scores equals +`overall_score` before clamping. Each sub-score is non-negative and within its +declared maximum (20, 20, 20, 20, 10, 10). + +**Validates: Requirements 1.2** + +Property 4: Fraud score boundedness — For all transactions, +`0 <= fraud_risk_score <= 100`. The score is capped with `min(100, score)` after +all rules are applied. + +**Validates: Requirements 1.3** + +Property 5: Fraud log idempotency — Evaluating the same transaction twice does not +create duplicate `FraudLog` rows. The second call is a no-op if a log already exists +for that `transaction_id`. + +**Validates: Requirements 1.3** + +Property 6: Cache transparency — For any endpoint with caching, the response returned +from cache is semantically identical to a freshly computed response for the same +`user_id` and parameters within the TTL window. + +**Validates: Requirements 1.4** + +Property 7: Chat memory cap — For any user, +`len(chat_memory.get_history(user_id)) <= 12` at all times. History is trimmed to +the last 12 messages after every addition. + +**Validates: Requirements 1.5** + +Property 8: Streaming completeness — Every `chat_start` WebSocket event is followed +by zero or more `chat_chunk` events and exactly one `chat_end` event per request, +regardless of which AI backend handles the response. + +**Validates: Requirements 1.5** + +Property 9: Insights non-empty — `analyze_spending_behavior` always returns at least +one insight string. When no anomalies are detected, a stable-spending confirmation +message is appended as the default. + +**Validates: Requirements 1.6** + +Property 10: Compound growth monotonicity — In `forecast_savings_and_investments`, +given `net_monthly > 0`, the `savings_growth` and `investment_growth` arrays are +monotonically non-decreasing across all months. + +**Validates: Requirements 1.1** + +Property 11: Debt decline monotonicity — The `debt_decline` array is monotonically +non-increasing. Debt is reduced by `monthly_debt_payment` each month and floored at +zero; it never increases in the projection model. + +**Validates: Requirements 1.1** + +Property 12: DB fallback transparency — All AI engine functions behave identically +whether the underlying engine is PostgreSQL or SQLite. No engine-specific SQL syntax +is used; all queries go through the SQLAlchemy ORM. + +**Validates: Requirements 1.7** + +Property 13: AI backend fallback completeness — `get_chat_response` and +`stream_chat_response` always return a non-empty response regardless of which AI +backends are available, including when all external backends are offline (the +offline rule-based fallback is invoked). + +**Validates: Requirements 1.8** + +Property 14: WebSocket multi-connection — A single `user_id` can have multiple +simultaneous WebSocket connections. `send_personal_message` delivers the message +to all active connections for that user. + +**Validates: Requirements 1.5** + +--- + +## Error Handling + +### Scenario 1: PostgreSQL Unavailable + +**Condition**: `OperationalError` raised during engine connection test at startup. +**Response**: `database.py` catches the exception, logs a warning, and re-initializes +the engine with `sqlite:///./bankbot.db` and `check_same_thread=False`. +**Recovery**: All subsequent DB operations use SQLite transparently. No restart required. + +--- + +### Scenario 2: Redis Unavailable + +**Condition**: `redis.Redis.ping()` raises an exception or `redis` library is not installed. +**Response**: `CacheManager.__init__` catches the exception, sets `use_redis = False`, +and all cache operations route to `MemoryCache`. +**Recovery**: In-memory cache is thread-safe via `threading.Lock`. TTL eviction is +lazy (checked on `get`). Cache is lost on process restart (acceptable for dev/staging). + +--- + +### Scenario 3: All AI Backends Offline + +**Condition**: `OPENAI_API_KEY` and `GROQ_API_KEY` are unset; local Ollama is unreachable. +**Response**: `has_active_ai_backend()` returns `False`. `get_chat_response` and +`generate_daily_briefing` invoke `get_offline_chat_fallback()` which generates a +rule-based, data-grounded response from the user's DB records. +**Recovery**: Responses are still financially meaningful (use real scores and balances). +No exception is surfaced to the API caller. + +--- + +### Scenario 4: User Not Found + +**Condition**: `user_id` query param is absent or references a non-existent user. +**Response**: `get_user_id_fallback()` in `router.py` queries the first available user. +If no users exist, raises `HTTPException(404, "No users found. Seed the database first.")`. +**Recovery**: Run `python backend/app/scripts/seed.py` to populate the database. + +--- + +### Scenario 5: WebSocket Client Disconnects Unexpectedly + +**Condition**: `WebSocketDisconnect` raised during `websocket.receive_text()`. +**Response**: `websocket/router.py` catches `WebSocketDisconnect`, calls +`ws_manager.disconnect(websocket, user_id)`, and closes the DB session in `finally`. +**Recovery**: Client can reconnect; server state is clean. Chat history is preserved +in `ChatMemoryManager` for the session duration. + +--- + +### Scenario 6: Transaction Not Found for Fraud Evaluation + +**Condition**: `POST /api/ai/fraud/evaluate/{transaction_id}` with a non-existent ID. +**Response**: `evaluate_transaction_for_fraud` returns `{"error": "Transaction not found"}`. +**Recovery**: Caller should verify the transaction ID before calling the endpoint. + +--- + +### Scenario 7: OpenAI / Groq API Error During Streaming + +**Condition**: Network error or rate limit during `stream_chat_response`. +**Response**: The exception is caught per-backend; the next backend in the fallback +chain is attempted. If all fail, `get_offline_chat_fallback()` result is yielded as +a single chunk. +**Recovery**: Streaming continues without interruption from the client's perspective. + +--- + +## Testing Strategy + +### Unit Testing Approach + +Each AI engine module is tested in isolation with a mocked SQLAlchemy session. +Key test cases per module: + +- **forecasting.py**: Zero-transaction user returns `(balance, 0.0, 0.0)`; + negative net daily floors projected balance at 0.0; chart_data length matches + `ceil(projection_days / 5) + 1`. +- **coaching.py**: Score is always in `[0, 100]`; improvements list is non-empty; + all six sub-scores are within their declared maximums. +- **fraud.py**: Score caps at 100 even when multiple rules fire simultaneously; + duplicate FraudLog is not created on second evaluation of same transaction. +- **behavior.py**: Returns at least one insight for any input including empty transaction list. +- **simulation.py**: `projected_balance >= 0` for any purchase amount; investment + growth projection always has exactly 3 entries. +- **cache.py**: MemoryCache TTL eviction works correctly; expired keys return `None`. + +### Property-Based Testing Approach + +**Property Test Library**: `hypothesis` (Python) + +Key properties to test with generated inputs: + +```python +from hypothesis import given, strategies as st + +@given(st.floats(min_value=0, max_value=1e9), st.floats(min_value=0, max_value=1e6)) +def test_projected_balance_non_negative(current_balance, net_daily_loss): + """projected_balance is always >= 0 regardless of net daily cashflow.""" + result = max(0.0, current_balance - net_daily_loss * 90) + assert result >= 0.0 + +@given(st.floats(min_value=0, max_value=1e6), st.floats(min_value=0, max_value=1e6), + st.floats(min_value=0, max_value=1e6), st.floats(min_value=0, max_value=1e6), + st.floats(min_value=0, max_value=1e6), st.floats(min_value=0, max_value=1e6)) +def test_health_score_bounded(s1, s2, s3, s4, s5, s6): + """Financial health score is always in [0, 100].""" + raw = s1 + s2 + s3 + s4 + s5 + s6 + score = min(100.0, max(0.0, raw)) + assert 0.0 <= score <= 100.0 + +@given(st.integers(min_value=0, max_value=500)) +def test_fraud_score_bounded(rule_score_sum): + """Fraud score is always capped at 100.""" + score = min(100, rule_score_sum) + assert 0 <= score <= 100 +``` + +### Integration Testing Approach + +1. **Database Fallback**: Start with `DATABASE_URL` pointing to an unreachable host; + verify `engine` uses SQLite and `Base.metadata.create_all()` succeeds. +2. **Seed + Endpoint Round-trip**: Run `seed.py`, then call all GET endpoints via + `httpx`; assert all return HTTP 200 with non-empty JSON bodies. +3. **WebSocket Chat**: Open a WebSocket connection, send a chat message, collect all + chunks until `chat_end`, assert the assembled reply is a non-empty string. +4. **Cache Hit Verification**: Call a cached endpoint twice; assert the second call + returns within 10ms (cache hit) and the response is identical. +5. **Fraud Idempotency**: Call `POST /api/ai/fraud/evaluate/{id}` twice for the same + transaction; assert only one `FraudLog` row exists in the DB. + +--- + +## Performance Considerations + +- **Cache TTLs are tuned by endpoint cost**: Briefings (LLM-heavy) cache for 3600s; + behavioral insights cache for 600s; balance projections cache for 300s. +- **Chat history is capped at 12 messages** to bound the token count sent to LLMs + and prevent context window overflow. +- **AI backend detection is done once at module load time** (`AI_BACKEND_AVAILABLE` + flag in `ollama_integration.py`) to avoid per-request timeout delays. +- **Fraud history window is capped at 30 transactions** to keep anomaly detection O(1) + in practice. +- **WebSocket streaming** avoids buffering the full LLM response before delivery, + reducing perceived latency for the user. +- **SQLite WAL mode** should be enabled for concurrent read performance if multiple + workers are used with the SQLite fallback. + +--- + +## Security Considerations + +- **No authentication on AI endpoints** in the current implementation — `user_id` is + passed as a query parameter. Production deployment must add JWT middleware to + validate that the requesting user can only access their own data. +- **API keys** (`OPENAI_API_KEY`, `GROQ_API_KEY`) are read from environment variables + and never logged or returned in API responses. +- **SQL injection** is not possible — all DB queries use SQLAlchemy ORM with + parameterized bindings. +- **WebSocket origin validation** is not enforced in development. Production should + restrict `allow_origins` in CORS middleware and validate WebSocket upgrade headers. +- **Fraud log writes** are the only DB mutations in the AI layer; all other operations + are read-only, limiting the blast radius of any AI module bug. +- **LLM prompt injection**: User chat messages are passed as `role: user` content, + not interpolated into the system prompt, reducing prompt injection risk. + +--- + +## Dependencies + +| Package | Purpose | Notes | +|---------|---------|-------| +| `fastapi` | HTTP and WebSocket framework | Core API layer | +| `uvicorn` | ASGI server | Run with `uvicorn app.main:app` | +| `sqlalchemy` | ORM and DB abstraction | PostgreSQL + SQLite | +| `psycopg2-binary` | PostgreSQL driver | Falls back gracefully if PG unavailable | +| `openai` | OpenAI API client | `gpt-4o-mini` default model | +| `groq` | Groq API client | `llama-3.3-70b-versatile` fallback | +| `requests` | HTTP client for Ollama | Local Ollama REST API | +| `redis` | Redis client | Optional; falls back to MemoryCache | +| `numpy` | Statistical computations | Mean, stddev for fraud/behavior | +| `langchain` | LLM orchestration | Listed in requirements; available for future chain-based features | +| `pydantic` | Request/response validation | Pydantic v2 compatible | +| `hypothesis` | Property-based testing | Dev dependency | +| `httpx` | Async HTTP client for tests | Dev dependency | +| `websockets` | WebSocket client for tests | Dev dependency | diff --git a/.kiro/specs/bankbot-ai-intelligence/tasks.md b/.kiro/specs/bankbot-ai-intelligence/tasks.md new file mode 100644 index 0000000000000000000000000000000000000000..89371acc24f3e761d69fef5884d97fd519b0aba1 --- /dev/null +++ b/.kiro/specs/bankbot-ai-intelligence/tasks.md @@ -0,0 +1,286 @@ +# Tasks: BankBot AI Intelligence & API (Phase 4) + +## Implementation Status Summary + +After a full codebase audit, the majority of Phase 4 is already implemented. The tasks below +reflect the **actual gaps** between the current code and the design specification — not a +from-scratch build. Each task is scoped to what genuinely needs to be done. + +--- + +## Task 1: Fix requirements.txt — Add Missing Dependencies + +**Status**: ✅ Complete +**Priority**: Critical (blocks everything else) +**File**: `backend/requirements.txt` + +The current `requirements.txt` is missing several packages that the AI layer actively imports. +Running the backend without these will cause `ImportError` crashes. + +- [ ] 1.1 Add `groq` — required by `ollama_integration.py` (`from groq import Groq`) +- [ ] 1.2 Add `redis` — required by `middleware/cache.py` (`import redis`) +- [ ] 1.3 Add `numpy` — required by `ai/fraud.py` and `ai/behavior.py` (`import numpy as np`) +- [ ] 1.4 Add `httpx` — required for endpoint validation scripts +- [ ] 1.5 Add `websockets` — required for WebSocket test script +- [ ] 1.6 Add `python-dotenv` — required to load `.env` file when running locally +- [ ] 1.7 Pin versions for all new additions (e.g. `groq==0.9.0`, `redis==5.0.4`, `numpy==1.26.4`) + +**Acceptance**: `pip install -r requirements.txt` completes without errors in a clean venv. + +--- + +## Task 2: Extend ollama_integration.py — Add OpenAI to Unified Wrapper + +**Status**: ✅ Complete +**Priority**: High +**File**: `backend/app/ai/ollama_integration.py` + +The `get_ai_response()` and `stream_ai_response()` unified wrappers currently only route to +Groq or Ollama. The design specifies OpenAI as the **first priority** in the fallback chain. +`chat.py` and `coaching.py` implement OpenAI directly, but `ollama_integration.py` — which +is the shared backend abstraction — does not. + +- [ ] 2.1 Add `get_openai_response(prompt, history, model, language)` function that calls + `openai.OpenAI(api_key=OPENAI_API_KEY).chat.completions.create(...)` with the configured + model (default `gpt-4o-mini`, read from `OPENAI_MODEL` env var) +- [ ] 2.2 Add `stream_openai_response(prompt, history, model, language)` generator that + calls the same endpoint with `stream=True` and yields content chunks +- [ ] 2.3 Update `get_ai_response()` to try OpenAI first, then Groq, then Ollama: + ```python + if OPENAI_API_KEY: + result = get_openai_response(...) + if result: return result + if GROQ_API_KEY: + result = get_groq_response(...) + if result: return result + return get_ollama_response(...) + ``` +- [ ] 2.4 Update `stream_ai_response()` with the same three-tier priority order +- [ ] 2.5 Update `AI_BACKEND_AVAILABLE` detection at module load to also check `OPENAI_API_KEY` + (it already does — verify the logic is correct and add a comment) +- [ ] 2.6 Add `OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")` constant at + the top of the file so the model is configurable without code changes + +**Acceptance**: With `OPENAI_API_KEY` set, `get_ai_response("test")` returns an OpenAI +response. With only `GROQ_API_KEY` set, it falls back to Groq. With neither, it falls back +to Ollama or returns `None`. + +--- + +## Task 3: Update .env.example — Document All AI & Cache Variables + +**Status**: ✅ Complete +**Priority**: High +**File**: `.env.example` + +The `.env.example` needs to document every environment variable the Phase 4 AI layer reads, +so any developer can get the app running immediately. + +- [ ] 3.1 Add `OPENAI_API_KEY=your_openai_key_here` +- [ ] 3.2 Add `OPENAI_MODEL=gpt-4o-mini` +- [ ] 3.3 Add `GROQ_API_KEY=your_groq_key_here` +- [ ] 3.4 Add `OLLAMA_MODEL=llama3:latest` +- [ ] 3.5 Add `REDIS_URL=redis://localhost:6379/0` +- [ ] 3.6 Add `USE_SQLITE=true` with a comment explaining it forces SQLite fallback +- [ ] 3.7 Add `DATABASE_URL=postgresql://admin:adminpassword@localhost:5432/bankbot` +- [ ] 3.8 Add a comment block at the top explaining the fallback priority chain: + `# AI: OpenAI → Groq → Ollama → offline fallback` + `# DB: PostgreSQL → SQLite` + `# Cache: Redis → in-memory TTL dict` + +**Acceptance**: A developer can copy `.env.example` to `.env`, fill in one API key, and +the backend starts without any missing-variable errors. + +--- + +## Task 4: Validate and Harden seed.py + +**Status**: ✅ Complete +**Priority**: Medium +**File**: `backend/app/scripts/seed.py` + +The seed script works but has one logic bug: the `db.commit()` inside the transaction loop +commits after every single transaction, which is slow and can leave partial state on error. +Also, the script does not print which DB backend it seeded into. + +- [ ] 4.1 Move `db.commit()` out of the per-transaction loop — commit once per user after + all their transactions, goals, investments, and subscriptions are added +- [ ] 4.2 Add a `try/except/rollback` block around the per-user seeding so a failure on + one user does not corrupt the others +- [ ] 4.3 Print the active database URL at the start of `seed_data()` so the developer + knows whether SQLite or PostgreSQL was used: + ```python + from app.database.database import SQLALCHEMY_DATABASE_URL + print(f"Seeding into: {SQLALCHEMY_DATABASE_URL}") + ``` +- [ ] 4.4 Add a second subscription per user (e.g. Spotify at $9.99/month with + `usage_frequency: "low"`) so the subscription optimizer has data to detect unused subs +- [ ] 4.5 Add a duplicate subscription for one user (two Netflix entries) so the duplicate + detection logic in `subscriptions.py` has a real test case +- [ ] 4.6 Add a late-night transaction (timestamp hour = 23 or 0) for at least one user + so `behavior.py` late-night detection fires on seeded data + +**Acceptance**: Running `python backend/app/scripts/seed.py` twice: first run seeds 5 users +and prints "Database seeded successfully!"; second run prints "Database already seeded." and +exits cleanly. The `subscriptions/optimize` endpoint returns at least one unused subscription +and one duplicate after seeding. + +--- + +## Task 5: Build test_endpoints.py — Full HTTP Validation Script + +**Status**: ✅ Complete +**Priority**: High +**File**: `backend/app/scripts/test_endpoints.py` + +The current `test_endpoints.py` needs to be a real validation script that calls every AI +endpoint and asserts the response shape is correct. + +- [ ] 5.1 Import `httpx` and define `BASE_URL = "http://localhost:8000"` +- [ ] 5.2 Add a `get_first_user_id()` helper that calls `GET /api/ai/coaching/score` without + a `user_id` param (uses the fallback) and extracts the user from the response, or queries + the DB directly via `seed.py`'s session +- [ ] 5.3 Test `GET /api/ai/twin/predict` — assert `200`, assert keys + `current_balance`, `projected_balance`, `chart_data` exist, assert `len(chart_data) >= 1` +- [ ] 5.4 Test `GET /api/ai/twin/future` — assert `200`, assert `savings_growth` and + `investment_growth` are non-empty lists +- [ ] 5.5 Test `GET /api/ai/twin/scenarios` — assert `200`, assert keys `status_quo`, + `frugal`, `lifestyle_inflation` all present +- [ ] 5.6 Test `POST /api/ai/simulate/purchase` with body + `{"amount": 500.0, "merchant": "Test", "category": "Shopping"}` — assert `200`, + assert `risk_analysis.risk_level` is one of `low/medium/high/critical` +- [ ] 5.7 Test `POST /api/ai/simulate/investment` with body + `{"monthly_sip": 200.0, "asset_type": "stock"}` — assert `200`, + assert `growth_projection` has exactly 3 entries (year 1, 3, 5) +- [ ] 5.8 Test `GET /api/ai/behavior/insights` — assert `200`, assert `insights` is a + non-empty list +- [ ] 5.9 Test `GET /api/ai/coaching/score` — assert `200`, assert `overall_score` is + between 0 and 100, assert all 6 category keys are present +- [ ] 5.10 Test `GET /api/ai/coaching/briefing` — assert `200`, assert `briefing` is a + non-empty string +- [ ] 5.11 Test `GET /api/ai/subscriptions/optimize` — assert `200`, assert `subscriptions` + key exists +- [ ] 5.12 Test `GET /api/ai/fraud/analysis` — assert `200`, assert `total_alerts` key exists +- [ ] 5.13 Test `POST /api/ai/chat` with body `{"message": "What is my savings rate?"}` — + assert `200`, assert `response` is a non-empty string +- [ ] 5.14 Print a pass/fail summary table at the end showing each endpoint and its result +- [ ] 5.15 Exit with code `1` if any test fails so CI can detect failures + +**Acceptance**: Running `python backend/app/scripts/test_endpoints.py` with the server +running prints a table where all 13 endpoints show PASS. + +--- + +## Task 6: Build test_websocket.py — WebSocket Streaming Validation Script + +**Status**: ✅ Complete +**Priority**: High +**File**: `backend/app/scripts/test_websocket.py` (new file) + +No WebSocket test script exists. This is required by the verification plan. + +- [ ] 6.1 Create `backend/app/scripts/test_websocket.py` +- [ ] 6.2 Import `asyncio`, `websockets`, `json` +- [ ] 6.3 Write `async def test_chat_streaming()`: + - Connect to `ws://localhost:8000/api/ai/chat/ws` + - Send `{"type": "chat", "message": "What is my current balance?"}` + - Collect all messages until `type == "chat_end"` + - Assert at least one `chat_chunk` was received + - Assert the assembled reply is a non-empty string + - Print the full assembled reply +- [ ] 6.4 Write `async def test_ping_pong()`: + - Connect to the same endpoint + - Send `{"type": "ping"}` + - Assert the response is `{"type": "pong"}` +- [ ] 6.5 Write `async def test_invalid_json()`: + - Send a raw non-JSON string + - Assert the response contains `{"type": "error"}` +- [ ] 6.6 Run all three tests in `asyncio.run(main())` and print pass/fail for each +- [ ] 6.7 Exit with code `1` if any test fails + +**Acceptance**: Running `python backend/app/scripts/test_websocket.py` with the server +running prints three PASS lines and exits with code 0. + +--- + +## Task 7: Add CORS Hardening and Health Endpoint to main.py + +**Status**: ✅ Complete +**Priority**: Low (dev environment acceptable, note for production) +**File**: `backend/app/main.py` + +- [ ] 7.1 Add a `GET /api/ai/status` endpoint that returns the active AI backend, DB type, + and cache type — useful for debugging without reading logs: + ```python + @app.get("/api/ai/status") + def ai_status(): + from app.ai.ollama_integration import has_active_ai_backend, OPENAI_API_KEY, GROQ_API_KEY + from app.middleware.cache import cache + from app.database.database import SQLALCHEMY_DATABASE_URL + return { + "ai_backend": "openai" if OPENAI_API_KEY else "groq" if GROQ_API_KEY else "ollama", + "ai_available": has_active_ai_backend(), + "db_type": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", + "cache_type": "redis" if cache.use_redis else "memory" + } + ``` +- [ ] 7.2 Add a comment above `allow_origins=["*"]` noting it must be restricted to the + frontend origin in production (e.g. `["http://localhost:3000"]`) + +**Acceptance**: `GET /api/ai/status` returns a JSON object with all four fields populated +correctly based on the active environment. + +--- + +## Task 8: Verify Full Stack Runs End-to-End + +**Status**: ✅ Complete — verified 2025-05-24 +**Priority**: Critical +**This is the final integration verification task.** + +- [ ] 8.1 Install dependencies: `pip install -r backend/requirements.txt` +- [ ] 8.2 Seed the database: `python backend/app/scripts/seed.py` + - Confirm output: `Seeding into: sqlite:///...` and `Database seeded successfully!` +- [ ] 8.3 Start the backend: `uvicorn app.main:app --reload` from `backend/` + - Confirm output: `Initializing database...` and `Database initialization complete.` + - Confirm no `ImportError` or `ModuleNotFoundError` in startup logs +- [ ] 8.4 Open Swagger UI at `http://localhost:8000/docs` + - Confirm all 13 AI endpoints appear under the `AI Intelligence` tag + - Confirm the WebSocket endpoint appears under `WebSockets` +- [ ] 8.5 Run HTTP validation: `python backend/app/scripts/test_endpoints.py` + - Confirm all 13 endpoints return PASS +- [ ] 8.6 Run WebSocket validation: `python backend/app/scripts/test_websocket.py` + - Confirm all 3 WebSocket tests return PASS +- [ ] 8.7 Check `GET /api/ai/status` returns correct backend/db/cache values +- [ ] 8.8 Manually call `GET /api/ai/coaching/score` via Swagger UI and confirm the + response contains `overall_score` between 0–100 and all 6 sub-score categories + +**Acceptance**: All 8 sub-tasks complete without errors. The backend is fully operational +with SQLite fallback and in-memory cache, ready for frontend integration. + +--- + +## Dependency Map + +``` +Task 1 (requirements.txt) + └── Task 2 (ollama_integration OpenAI) + └── Task 5 (test_endpoints — needs httpx) + └── Task 6 (test_websocket — needs websockets) + +Task 3 (.env.example) — independent + +Task 4 (seed.py hardening) + └── Task 8.2 (seeding step in final verification) + +Task 2 (ollama_integration) + └── Task 8 (full stack verification) + +Task 5 + Task 6 (test scripts) + └── Task 8.5 + 8.6 + +Task 7 (status endpoint) + └── Task 8.7 +``` + +**Recommended execution order**: 1 → 3 → 4 → 2 → 7 → 5 → 6 → 8 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..7a73a41bfdf76d6f793007240d80983a52f15f97 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..39a6b43250477f3e8069d46bd5e099d73b75ddea --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +# ============================================================ +# BankBot AI — Hugging Face Spaces Dockerfile +# +# Single-container deployment: +# Port 7860 (HF requirement) → Nginx +# Nginx → Next.js (port 3000) for frontend +# Nginx → FastAPI (port 8000) for /api/* and /ws +# +# Build args: +# NEXT_PUBLIC_API_URL (default: relative /api proxy) +# ============================================================ + +# ─── Stage 1: Build Next.js frontend ───────────────────────────────────────── +FROM node:20-alpine AS frontend-builder + +WORKDIR /frontend + +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm ci --legacy-peer-deps --quiet + +COPY frontend/ . + +# In HF, frontend calls go through the same origin via Nginx proxy +# So NEXT_PUBLIC_API_URL is empty — rewrites handle /api/* internally +ARG NEXT_PUBLIC_API_URL="" +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN npm run build + +# ─── Stage 2: Python dependencies ──────────────────────────────────────────── +FROM python:3.11-slim AS python-builder + +WORKDIR /build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + +# ─── Stage 3: Final runtime image ──────────────────────────────────────────── +FROM python:3.11-slim AS runtime + +# Install Node.js, Nginx, supervisord, curl +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + supervisor \ + curl \ + libpq5 \ + ca-certificates \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# ── Python packages ─────────────────────────────────────────────────────────── +COPY --from=python-builder /install /usr/local + +# ── Backend ─────────────────────────────────────────────────────────────────── +WORKDIR /app/backend +COPY backend/app/ ./app/ +COPY backend/requirements.txt . + +# ── Frontend (standalone build) ─────────────────────────────────────────────── +WORKDIR /app/frontend +COPY --from=frontend-builder /frontend/.next/standalone ./ +COPY --from=frontend-builder /frontend/.next/static ./.next/static +COPY --from=frontend-builder /frontend/public ./public + +# ── Nginx config ────────────────────────────────────────────────────────────── +COPY hf/nginx.conf /etc/nginx/nginx.conf + +# ── Supervisord config ──────────────────────────────────────────────────────── +COPY hf/supervisord.conf /etc/supervisor/conf.d/bankbot.conf + +# ── Startup script ──────────────────────────────────────────────────────────── +COPY hf/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# ── Writable dirs for non-root (HF runs as user 1000) ──────────────────────── +RUN mkdir -p /app/data /var/log/supervisor /var/log/nginx /var/lib/nginx/body \ + && chmod -R 777 /app/data /var/log/supervisor /var/log/nginx \ + && chmod -R 777 /var/lib/nginx \ + && touch /run/nginx.pid && chmod 777 /run/nginx.pid \ + && chown -R 1000:1000 /app /var/log/supervisor /var/log/nginx /var/lib/nginx + +# HF Spaces requires port 7860 +EXPOSE 7860 + +# Health check +HEALTHCHECK --interval=30s --timeout=15s --start-period=60s --retries=5 \ + CMD curl -f http://localhost:7860/health || exit 1 + +# Run as user 1000 (HF default) +USER 1000 + +WORKDIR /app + +CMD ["/app/start.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..73ddac48bb1d5301812907772c19af9d6b3ab588 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +--- +title: BankBot AI +emoji: 🏦 +colorFrom: blue +colorTo: green +sdk: docker +pinned: true +license: mit +short_description: AI-Native Financial Operating System — real-time streaming, fraud detection, forecasting +--- + +
+ +# 🏦 BankBot AI + +### AI-Native Financial Operating System + +[![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com) +[![Next.js](https://img.shields.io/badge/Next.js_14-black?style=flat-square&logo=next.js&logoColor=white)](https://nextjs.org) +[![Python](https://img.shields.io/badge/Python_3.11-3776AB?style=flat-square&logo=python&logoColor=white)](https://python.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://typescriptlang.org) +[![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat-square&logo=docker&logoColor=white)](https://docker.com) +[![OpenAI](https://img.shields.io/badge/OpenAI-412991?style=flat-square&logo=openai&logoColor=white)](https://openai.com) + +**A production-grade AI fintech platform** with real-time WebSocket streaming, multi-provider AI fallback, fraud detection, financial forecasting, and a premium glassmorphism UI. + +
+ +--- + +## 🚀 Demo + +**Login with the demo account:** +``` +Email: alex@bankbot.dev +Password: BankBot2026! +``` + +The demo account includes: +- **$59,637** across 3 accounts (checking · savings · investment) +- **301 transactions** across 6 months +- **1 fraud alert** (Tech Store NYC, $847, 78% risk score) +- **4 financial goals** (Emergency Fund · Vacation · MacBook · Down Payment) +- **4 investments** (S&P 500 · AAPL · BTC · Treasury Bonds) +- **6 notifications** (3 unread) + +--- + +## ✨ Features + +### 🤖 AI Financial Twin +- **Contextual chat** — AI knows your real balance, goals, investments, and spending patterns +- **4-tier AI fallback**: OpenAI → Groq → Ollama → Rule-based (always responds) +- **Real-time streaming** via WebSocket — character-by-character with auto-reconnect + +### 📊 Financial Intelligence +- **Health Score** — 100-point composite across 6 dimensions +- **What-If Simulator** — 6 sliders, instant 36-month projection +- **Spending Heatmap** — weekly behavioral patterns +- **Category Intelligence** — AI insights per spending category + +### 🛡️ Fraud Detection +- **Real-time scoring** — amount spikes, timing anomalies, rapid-fire, duplicates +- **Risk levels** — verified / suspicious / flagged +- **Live alerts** — notification panel with unread count + +### ⚡ Performance +- Dashboard: **65ms cold, 10ms cached** +- Cache-aside: Redis → in-memory fallback (automatic) +- All data endpoints: **< 20ms** warm + +### 🔍 Observability +- Live metrics at `/api/metrics` +- System Status page at `/status` +- Structured JSON logging with request tracing + +--- + +## 🏗️ Architecture + +``` +Browser (port 7860) + │ + ▼ +Nginx (port 7860) — single entry point + │ │ + ▼ ▼ +Next.js (3000) FastAPI (8000) + │ │ + └────────────────────┤ + │ + ┌──────────┴──────────┐ + │ │ + SQLite/PostgreSQL Redis/Memory + (auto-fallback) (auto-fallback) + │ + ┌──────────┴──────────┐ + │ │ │ + OpenAI Groq Ollama + (P1) (P2) (P3) + Rule-based (P4) +``` + +--- + +## ⚙️ Configuration (HF Secrets) + +Set these in your Space's **Settings → Repository secrets**: + +| Secret | Required | Description | +|--------|----------|-------------| +| `OPENAI_API_KEY` | Optional* | OpenAI GPT-4o-mini | +| `GROQ_API_KEY` | Optional* | Groq llama-3.3-70b (free) | +| `JWT_SECRET_KEY` | Recommended | JWT signing secret | +| `DATABASE_URL` | Optional | External PostgreSQL (Neon/Supabase) | +| `REDIS_URL` | Optional | External Redis | + +*At least one AI key recommended. Without any key, the app uses rule-based responses from your actual financial data. + +**Get a free Groq key:** https://console.groq.com/keys + +--- + +## 🗄️ Database Options + +### Option 1: SQLite (Default — works out of the box) +No configuration needed. Data resets on Space restart (fine for demo). + +### Option 2: Neon PostgreSQL (Persistent) +1. Create free DB at https://neon.tech +2. Set `DATABASE_URL` secret: `postgresql://user:pass@ep-xxx.neon.tech/bankbot?sslmode=require` + +### Option 3: Supabase PostgreSQL (Persistent) +1. Create project at https://supabase.com +2. Set `DATABASE_URL` from Settings → Database → Connection string + +--- + +## 📡 API Endpoints + +``` +GET /health Health check +GET /api/status Runtime info +GET /api/metrics Live observability +GET /docs Interactive API docs + +POST /api/auth/login Login → JWT +POST /api/auth/register Register +GET /api/dashboard/overview Full dashboard (65ms) +GET /api/transactions/ Transaction history +GET /api/notifications/ Notifications +GET /api/ai/coaching/score Health score +GET /api/ai/fraud/analysis Fraud alerts +POST /api/ai/chat HTTP chat +WS /api/ai/chat/ws Streaming chat +``` + +--- + +## 🛠️ Tech Stack + +| Layer | Technology | +|-------|-----------| +| Frontend | Next.js 14, TypeScript, Tailwind CSS | +| Animation | Framer Motion | +| Charts | Recharts | +| State | Zustand | +| Backend | FastAPI, Python 3.11 | +| Database | PostgreSQL / SQLite fallback | +| Cache | Redis / in-memory fallback | +| Auth | JWT (python-jose), bcrypt | +| AI | OpenAI / Groq / Ollama / Rule-based | +| Container | Docker (single container) | +| Proxy | Nginx (port 7860) | + +--- + +## 📁 Source Code + +Full source: [GitHub Repository](https://github.com/your-username/bankbot-ai) + +Documentation: +- [Architecture](./docs/ARCHITECTURE.md) +- [API Reference](./docs/API_DOCUMENTATION.md) +- [Deployment Guide](./docs/DEPLOYMENT_GUIDE.md) +- [ER Diagram](./docs/ER_DIAGRAM.md) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..eecc7b1adff5e40016e9710686cb311502feb509 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,44 @@ +# ─── Stage 1: Builder ──────────────────────────────────────────────────────── +FROM python:3.11-slim AS builder + +WORKDIR /build + +# System deps for psycopg2 and cryptography +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + +# ─── Stage 2: Runtime ──────────────────────────────────────────────────────── +FROM python:3.11-slim AS runtime + +WORKDIR /app + +# Runtime system deps only +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy installed packages from builder +COPY --from=builder /install /usr/local + +# Copy application code (exclude venv, __pycache__, .env) +COPY app/ ./app/ +COPY requirements.txt . + +# Non-root user for security +RUN useradd -m -u 1001 bankbot && chown -R bankbot:bankbot /app +USER bankbot + +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Production: no --reload, multiple workers +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--proxy-headers", "--forwarded-allow-ips", "*"] diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..3b34b39b5e0ad0502fe6f5018c6165af1ac32ba7 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,80 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date within the migration file +# timezone = UTC + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://admin:adminpassword@localhost:5432/bankbot + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000000000000000000000000000000000000..992be3c148f79544fd4180b00aa7a750562739fa --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,81 @@ +from logging.config import fileConfig +import os +import sys + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# Add parent directory to path to import app modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.database.models import Base +from app.database.database import SQLALCHEMY_DATABASE_URL + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + +# Set sqlalchemy.url from the environment or default +config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..fbc4b07dcef98b20c6f96b642097f35e8433258e --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/ai/behavior.py b/backend/app/ai/behavior.py new file mode 100644 index 0000000000000000000000000000000000000000..108befdf222238e4ac36865f2b19978420a001ea --- /dev/null +++ b/backend/app/ai/behavior.py @@ -0,0 +1,138 @@ +from datetime import datetime, timedelta +from collections import defaultdict +import numpy as np +from sqlalchemy.orm import Session +from app.database.models import Account, Transaction + +def analyze_spending_behavior(db: Session, user_id: str, days: int = 90): + """ + Analyzes historical transactions to detect behavioral patterns (late-night, impulsive, dopamine, stress). + """ + accounts = db.query(Account).filter(Account.user_id == user_id).all() + account_ids = [acc.id for acc in accounts] + + if not account_ids: + return {"insights": [], "metrics": {}} + + cutoff = datetime.utcnow() - timedelta(days=days) + txns = db.query(Transaction).filter( + Transaction.account_id.in_(account_ids), + Transaction.timestamp >= cutoff, + Transaction.type == "debit" + ).all() + + if not txns: + return { + "insights": ["No recent debit transactions to analyze. Complete a few purchases to start behavioral profiling."], + "metrics": { + "late_night_count": 0, + "late_night_total": 0.0, + "weekend_pct": 0.0, + "impulsive_count": 0, + "impulsive_total": 0.0, + "dopamine_count": 0, + "stress_count": 0 + } + } + + # Analyze variables + late_night_txns = [] + weekend_txns = [] + impulsive_txns = [] + dopamine_txns = [] + stress_txns = [] + + amounts = [t.amount for t in txns] + avg_txn = np.mean(amounts) + std_txn = np.std(amounts) if len(amounts) > 1 else 0.0 + + category_totals = defaultdict(float) + hourly_counts = defaultdict(int) + + for t in txns: + # Categorize + category_totals[t.category or "Other"] += t.amount + + # Timing + hour = t.timestamp.hour + hourly_counts[hour] += 1 + + # Late-night spending (11PM to 4AM) + if hour >= 23 or hour < 4: + late_night_txns.append(t) + + # Weekend spending (Friday, Saturday, Sunday) + # weekday() is 0=Monday, 4=Friday, 5=Saturday, 6=Sunday + day = t.timestamp.weekday() + if day in [4, 5, 6]: + weekend_txns.append(t) + + # Impulsive spending (More than average + 1.5 * standard dev, or marked as 'regret') + if t.amount > (avg_txn + 1.5 * std_txn) and (t.category in ["Shopping", "Entertainment", "Food"]): + impulsive_txns.append(t) + + # Emotion tags + emotion = (t.spending_emotion_label or "").lower() + if emotion == "regret": + stress_txns.append(t) + elif emotion in ["happy", "dopamine"] or (t.category == "Shopping" and t.amount > avg_txn): + dopamine_txns.append(t) + + # Insights construction + insights = [] + + # 1. Late night alert + late_night_pct = (len(late_night_txns) / len(txns) * 100) if txns else 0 + if late_night_pct > 15: + total_late = sum(t.amount for t in late_night_txns) + insights.append( + f"🌙 High late-night spending: {late_night_pct:.1f}% of transactions occur after 11PM (Total: ${total_late:,.2f}). " + "Consider setting a bedtime blocker on your bank card." + ) + + # 2. Weekend overspending + weekend_pct = (len(weekend_txns) / len(txns) * 100) if txns else 0 + if weekend_pct > 45: + weekend_avg = np.mean([t.amount for t in weekend_txns]) if weekend_txns else 0 + weekday_txns = [t for t in txns if t not in weekend_txns] + weekday_avg = np.mean([t.amount for t in weekday_txns]) if weekday_txns else 0 + + if weekend_avg > weekday_avg * 1.2: + pct_diff = ((weekend_avg - weekday_avg) / weekday_avg) * 100 + insights.append( + f"🎉 Weekend Spikes: You spend {pct_diff:.1f}% more on weekends than weekdays. " + "Mainly driven by dining out and recreational purchases." + ) + + # 3. Dopamine triggers + if len(dopamine_txns) > 3: + insights.append( + f"🛍️ Dopamine Spending: Detected {len(dopamine_txns)} shopping spikes. " + "These purchases often occur in bursts, indicating reward-seeking behavior." + ) + + # 4. Stress/Regret Spending + if len(stress_txns) > 0: + insights.append( + f"⚠️ Emotional Spending: You flagged {len(stress_txns)} transactions as 'regret' or 'stress spending'. " + "Implementing a 24-hour cooling-off rule for non-essential items over $100 could help." + ) + + # General fallback if no major insights + if not insights: + insights.append("📊 Spending Discipline: Your transactions exhibit stable and regular timing, with minimal signs of emotional or impulsive spending.") + + return { + "insights": insights, + "metrics": { + "late_night_count": len(late_night_txns), + "late_night_total": round(sum(t.amount for t in late_night_txns), 2), + "weekend_pct": round(weekend_pct, 2), + "impulsive_count": len(impulsive_txns), + "impulsive_total": round(sum(t.amount for t in impulsive_txns), 2), + "dopamine_count": len(dopamine_txns), + "stress_count": len(stress_txns), + "avg_transaction_amount": round(avg_txn, 2) + }, + "category_breakdown": {cat: round(amt, 2) for cat, amt in category_totals.items()} + } diff --git a/backend/app/ai/budget_planner.py b/backend/app/ai/budget_planner.py new file mode 100644 index 0000000000000000000000000000000000000000..5befa5f66a030480ae6c86f04249ee259185756b --- /dev/null +++ b/backend/app/ai/budget_planner.py @@ -0,0 +1,303 @@ +""" +Smart Budget Planner for BankBot +Categorizes spending and provides budgeting insights +""" + +import json +import os +import numpy as np +import pandas as pd +from datetime import datetime, timedelta +from collections import defaultdict +import uuid + +BUDGET_FILE = "budgets.json" + +# Category keywords for automatic categorization +CATEGORY_KEYWORDS = { + "Food & Dining": ["restaurant", "food", "cafe", "pizza", "burger", "biryani", "zomato", "swiggy", "coffee", "tea", "meal"], + "Shopping": ["shop", "store", "mall", "amazon", "flipkart", "ebay", "retail", "boutique", "apparel", "clothes"], + "Travel": ["uber", "taxi", "bus", "flight", "train", "travel", "hotel", "airline", "booking", "transport"], + "Entertainment": ["movie", "cinema", "game", "netflix", "spotify", "music", "ticket", "concert", "show"], + "Bills & Utilities": ["electricity", "water", "gas", "internet", "mobile", "phone", "bill", "subscription"], + "Healthcare": ["hospital", "doctor", "pharmacy", "medical", "health", "clinic", "medicine"], + "Groceries": ["grocery", "supermarket", "vegetables", "fruits", "milk", "wheat", "bazar"], + "Fitness": ["gym", "yoga", "fitness", "sports", "training", "coach"], + "Insurance": ["insurance", "premium", "policy"], + "Education": ["school", "college", "course", "book", "tuition", "fees"], + "Loan & EMI": ["loan", "emi", "mortgage", "credit"], + "Transfer": ["transfer", "sent", "payment"] +} + +class BudgetPlanner: + """Smart budget planning and expense tracking""" + + def __init__(self): + self.budgets = self.load_budgets() + + def load_budgets(self): + """Load saved budgets from file""" + if os.path.exists(BUDGET_FILE): + try: + with open(BUDGET_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading budgets: {e}") + return {} + return {} + + def save_budgets(self): + """Save budgets to file""" + try: + with open(BUDGET_FILE, "w", encoding="utf-8") as f: + json.dump(self.budgets, f, indent=4, ensure_ascii=False) + except Exception as e: + print(f"Error saving budgets: {e}") + + def categorize_transaction(self, description, amount=0): + """ + Automatically categorize a transaction based on description + Returns: Category name + """ + description_lower = description.lower() + + # Check against keywords + for category, keywords in CATEGORY_KEYWORDS.items(): + if any(keyword in description_lower for keyword in keywords): + return category + + # Default category + return "Other" + + def set_budget_limit(self, username, category, limit): + """Set budget limit for a spending category""" + if username not in self.budgets: + self.budgets[username] = {} + + self.budgets[username][category] = { + "limit": limit, + "created_at": datetime.now().isoformat(), + "alerts": [] + } + self.save_budgets() + + def analyze_spending(self, username, transactions, period_days=30): + """ + Analyze spending by category for a given period + Returns: Categorized spending data + """ + if not transactions: + return {} + + # Filter transactions from last N days + cutoff_date = datetime.now() - timedelta(days=period_days) + recent_txns = [] + + for txn in transactions: + try: + txn_date = datetime.fromisoformat(txn.get('date', '')) + if txn_date > cutoff_date and txn.get('type') == 'debit': + recent_txns.append(txn) + except: + pass + + # Categorize transactions + spending_by_category = defaultdict(float) + categorized_txns = defaultdict(list) + + for txn in recent_txns: + category = self.categorize_transaction( + txn.get('description', txn.get('details', '')), + float(txn.get('amount', 0)) + ) + amount = float(txn.get('amount', 0)) + spending_by_category[category] += amount + categorized_txns[category].append({ + 'date': txn.get('date'), + 'amount': amount, + 'details': txn.get('details', '') + }) + + return { + "period_days": period_days, + "spending_by_category": dict(spending_by_category), + "categorized_transactions": dict(categorized_txns), + "total_spending": sum(spending_by_category.values()), + "transaction_count": len(recent_txns) + } + + def check_budget_alerts(self, username, spending_analysis): + """Check if any spending categories exceed their budgets""" + alerts = [] + + if username not in self.budgets: + return alerts + + user_budgets = self.budgets.get(username, {}) + spending = spending_analysis.get('spending_by_category', {}) + + for category, budget_info in user_budgets.items(): + if category not in spending: + continue + + spent = spending[category] + limit = budget_info.get('limit', 0) + + if spent > limit: + percentage = (spent / limit) * 100 + alerts.append({ + "category": category, + "spent": round(spent, 2), + "limit": limit, + "percentage": round(percentage, 1), + "excess": round(spent - limit, 2), + "severity": "high" if percentage > 150 else "medium" if percentage > 100 else "low", + "timestamp": datetime.now().isoformat() + }) + + return alerts + + def generate_budget_plan(self, username, transactions, monthly_income=50000): + """Generate recommended budget plan based on spending patterns""" + spending_analysis = self.analyze_spending(username, transactions, period_days=90) + spending = spending_analysis.get('spending_by_category', {}) + + total_spending = spending_analysis.get('total_spending', 0) + avg_monthly_spending = total_spending / 3 if total_spending > 0 else 0 + + # Calculate budget percentages (50/30/20 rule variant) + recommended_budgets = {} + + if spending: + for category, amount in spending.items(): + percentage = (amount / total_spending * 100) if total_spending > 0 else 0 + recommended_budget = (percentage / 100) * monthly_income + recommended_budgets[category] = round(recommended_budget, 2) + + # Add default categories if not present + default_categories = { + "Food & Dining": monthly_income * 0.08, + "Shopping": monthly_income * 0.10, + "Travel": monthly_income * 0.08, + "Bills & Utilities": monthly_income * 0.15, + "Entertainment": monthly_income * 0.05, + "Savings": monthly_income * 0.20, + } + + for category, amount in default_categories.items(): + if category not in recommended_budgets: + recommended_budgets[category] = amount + + return { + "monthly_income": monthly_income, + "current_monthly_avg": round(avg_monthly_spending, 2), + "recommended_budgets": recommended_budgets, + "savings_potential": round(monthly_income - avg_monthly_spending, 2), + "budget_breakdown": { + "essentials": round(monthly_income * 0.50, 2), # Bills, groceries, insurance + "lifestyle": round(monthly_income * 0.30, 2), # Entertainment, dining, shopping + "savings": round(monthly_income * 0.20, 2) # Emergency fund, investments + } + } + + def predict_monthly_spending(self, username, transactions): + """ + Predict future spending using historical data + Returns: Predicted spending for next month + """ + if not transactions: + return {} + + # Analyze last 3 months + predictions = {} + + for period in [30, 60, 90]: + analysis = self.analyze_spending(username, transactions, period_days=period) + spending = analysis.get('spending_by_category', {}) + + # Calculate trends + for category, amount in spending.items(): + if category not in predictions: + predictions[category] = [] + predictions[category].append(amount) + + # Calculate averages and trends + predicted_spending = {} + for category, amounts in predictions.items(): + if amounts: + predicted_spending[category] = { + "predicted": round(np.mean(amounts), 2), + "trend": "increasing" if amounts[-1] > amounts[0] else "decreasing", + "variance": round(np.std(amounts), 2) + } + + return predicted_spending + + def get_savings_suggestions(self, username, spending_analysis, monthly_income=50000): + """Generate specific savings suggestions""" + suggestions = [] + spending = spending_analysis.get('spending_by_category', {}) + + # Check each category and provide suggestions + for category, amount in spending.items(): + percentage = (amount / monthly_income) * 100 if monthly_income > 0 else 0 + + if category == "Food & Dining" and percentage > 10: + reduction = amount - (monthly_income * 0.08) + suggestions.append({ + "category": "Food & Dining", + "potential_savings": round(reduction, 2), + "suggestion": f"You can save ₹{round(reduction, 2)} by reducing dining expenses by 10%", + "priority": "high" if reduction > 1000 else "medium" + }) + + elif category == "Shopping" and percentage > 12: + reduction = amount - (monthly_income * 0.10) + suggestions.append({ + "category": "Shopping", + "potential_savings": round(reduction, 2), + "suggestion": f"Reduce impulse purchases to save ₹{round(reduction, 2)} monthly", + "priority": "high" if reduction > 1000 else "medium" + }) + + elif category == "Entertainment" and percentage > 7: + reduction = amount - (monthly_income * 0.05) + suggestions.append({ + "category": "Entertainment", + "potential_savings": round(reduction, 2), + "suggestion": f"Optimize subscriptions and entertainment to save ₹{round(reduction, 2)}", + "priority": "low" + }) + + # Overall savings tip + total_savings = sum(s.get('potential_savings', 0) for s in suggestions) + if total_savings > 0: + suggestions.append({ + "category": "Total Potential Savings", + "potential_savings": round(total_savings, 2), + "suggestion": f"By following these suggestions, you can save ₹{round(total_savings, 2)} per month", + "priority": "high" + }) + + return suggestions + +def get_budget_insights(username, transactions, users_data): + """Get comprehensive budget insights for a user""" + planner = BudgetPlanner() + + user_data = users_data.get(username, {}) + monthly_income = user_data.get('monthly_income', 50000) + + spending_analysis = planner.analyze_spending(username, transactions) + budget_alerts = planner.check_budget_alerts(username, spending_analysis) + budget_plan = planner.generate_budget_plan(username, transactions, monthly_income) + savings_suggestions = planner.get_savings_suggestions(username, spending_analysis, monthly_income) + predicted_spending = planner.predict_monthly_spending(username, transactions) + + return { + "spending_analysis": spending_analysis, + "budget_alerts": budget_alerts, + "budget_plan": budget_plan, + "savings_suggestions": savings_suggestions, + "predicted_spending": predicted_spending + } diff --git a/backend/app/ai/chat.py b/backend/app/ai/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..b37ca0e923789a9a3cac821d6440aa768b52cf8e --- /dev/null +++ b/backend/app/ai/chat.py @@ -0,0 +1,289 @@ +import json +import os +from threading import Lock +from sqlalchemy.orm import Session +from app.database.models import User, Account, Transaction, Goal, Investment, Subscription +from app.ai.behavior import analyze_spending_behavior +from app.ai.coaching import calculate_financial_health_score +from app.ai.ollama_integration import get_groq_response, get_ollama_response, stream_groq_response, stream_ollama_response + +# Thread-safe chatbot memory storage +class ChatMemoryManager: + def __init__(self): + self._history = {} + self._lock = Lock() + + def get_history(self, user_id: str): + with self._lock: + if user_id not in self._history: + self._history[user_id] = [] + return self._history[user_id] + + def add_message(self, user_id: str, role: str, content: str): + with self._lock: + if user_id not in self._history: + self._history[user_id] = [] + self._history[user_id].append({"role": role, "content": content}) + # Limit history to last 12 messages (6 rounds) + if len(self._history[user_id]) > 12: + self._history[user_id] = self._history[user_id][-12:] + + def clear_history(self, user_id: str): + with self._lock: + if user_id in self._history: + self._history[user_id] = [] + +chat_memory = ChatMemoryManager() + +def build_user_context_string(db: Session, user_id: str) -> str: + """ + Queries database for a user's entire financial situation to construct a precise system context. + """ + user = db.query(User).filter(User.id == user_id).first() + if not user: + return "No user information available." + + accounts = db.query(Account).filter(Account.user_id == user_id).all() + total_balance = sum(acc.balance for acc in accounts) + account_details = [f"{acc.type.capitalize()} Account: ${acc.balance:,.2f}" for acc in accounts] + + goals = db.query(Goal).filter(Goal.user_id == user_id).all() + goals_details = [f"Goal '{g.title}': Target ${g.target_amount:,.2f}, Saved ${g.current_amount:,.2f}" for g in goals] + + investments = db.query(Investment).filter(Investment.user_id == user_id).all() + investments_details = [f"{i.asset_name} ({i.type}): invested ${i.amount_invested:,.2f}, Current Value ${i.current_value:,.2f}" for i in investments] + + subs = db.query(Subscription).filter(Subscription.user_id == user_id, Subscription.active == True).all() + subs_details = [f"{s.merchant}: ${s.amount:,.2f}/{s.billing_cycle}" for s in subs] + + # Run behavioral diagnostics + behavior = analyze_spending_behavior(db, user_id) + behavior_insights = behavior.get("insights", []) + + # Financial Score + score_data = calculate_financial_health_score(db, user_id) + financial_score = score_data.get("overall_score", 50) + + context = f""" + User Profile: + - Name: {user.profile_data.get('name', 'Client')} + - Financial Personality: {user.financial_personality} + - Financial Health Score: {financial_score:.0f}/100 + + Balances: + {', '.join(account_details) if account_details else 'No active bank accounts'} + - Total Liquid Capital: ${total_balance:,.2f} + + Financial Goals: + {'; '.join(goals_details) if goals_details else 'None established'} + + Active Portfolio: + {'; '.join(investments_details) if investments_details else 'No active investments'} + + Active Subscriptions: + {'; '.join(subs_details) if subs_details else 'No active subscriptions'} + + Diagnostics & Behavior: + - {'; '.join(behavior_insights)} + - Late night spending occurrences: {behavior.get('metrics', {}).get('late_night_count', 0)} + - Weekend spending ratio: {behavior.get('metrics', {}).get('weekend_pct', 0.0)}% + """ + return context + +def get_contextual_system_prompt(db: Session, user_id: str) -> str: + """ + Constructs a highly specific system prompt containing the user's financial profile. + """ + financial_context = build_user_context_string(db, user_id) + + system_prompt = f"""You are BankBot, an elite AI Financial Analyst, Wealth Advisor, and Predictive Banking Engine. + You communicate with the user, providing highly personalized, concise, and mathematically rigorous answers. + You have direct, read-only access to the client's current financial profile and database records. + + CURRENT USER PORTFOLIO DATA: + {financial_context} + + CORE PRINCIPLES: + 1. NEVER behave like a generic chatbot. Avoid generic suggestions like "save more money". Use real numbers, calculate percentages, and suggest specific actions based on the client's data. + 2. Respond with the authority and brevity of a Bloomberg Terminal analyst. + 3. Keep your answers brief, actionable, and financially meaningful (typically 2-4 sentences max). + 4. If the user asks a question about their spending, goals, or predictions, use the portfolio data above. + 5. Always remain helpful, professional, and secure. + """ + return system_prompt + +def get_offline_chat_fallback(db: Session, user_id: str, prompt: str) -> str: + """ + Generates a localized, rule-grounded financial analyst reply when AI engines are offline. + """ + user = db.query(User).filter(User.id == user_id).first() + persona = user.financial_personality if user else "Saver" + + prompt_lower = prompt.lower() + + if "discipline" in prompt_lower or "spend" in prompt_lower or "budget" in prompt_lower: + score_data = calculate_financial_health_score(db, user_id) + discipline_score = score_data.get("categories", {}).get("spending_discipline", {}).get("score", 10) + return ( + f"As a {persona}, your spending discipline score stands at {discipline_score:.0f}/20. " + f"Analysis of your transaction history shows discretionary spikes. " + "To optimize your cashflow surplus, establish a strict 20% savings buffer prior to discretionary outflow." + ) + elif "investment" in prompt_lower or "portfolio" in prompt_lower or "grow" in prompt_lower: + investments = db.query(Investment).filter(Investment.user_id == user_id).all() + inv_total = sum(i.current_value for i in investments) + return ( + f"Your current investment portfolio valuation stands at ${inv_total:,.2f}. " + "Based on asset performance, shifting 15% of your net checking surplus into stock index funds " + "will counter inflation and capture a projected 8% compound annual return." + ) + else: + score_data = calculate_financial_health_score(db, user_id) + score = score_data.get("overall_score", 50) + return ( + f"Wealth Advisor assessment: Your overall Financial Health Score is {score:.0f}/100. " + "Liquidity is stable, but subscription and discretionary leakages are tempering compounding growth. " + "Audit duplicate subscriptions and automate goal savings to enhance your trajectory." + ) + +def get_chat_response(db: Session, user_id: str, prompt: str) -> str: + """ + Returns an HTTP conversational response grounded in database context. + """ + from app.ai.ollama_integration import has_active_ai_backend + + if not has_active_ai_backend(): + fallback_msg = get_offline_chat_fallback(db, user_id, prompt) + chat_memory.add_message(user_id, "user", prompt) + chat_memory.add_message(user_id, "assistant", fallback_msg) + return fallback_msg + + sys_prompt = get_contextual_system_prompt(db, user_id) + history = chat_memory.get_history(user_id) + + # Construct complete prompt for underlying backend + full_messages = [{"role": "system", "content": sys_prompt}] + for msg in history: + full_messages.append({"role": msg["role"], "content": msg["content"]}) + full_messages.append({"role": "user", "content": prompt}) + + # Determine backend + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") + GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") + + response_content = None + + if OPENAI_API_KEY: + try: + from openai import OpenAI + client = OpenAI(api_key=OPENAI_API_KEY) + res = client.chat.completions.create( + model="gpt-4o-mini", + messages=full_messages, + temperature=0.1, + max_tokens=500 + ) + response_content = res.choices[0].message.content + except Exception as e: + print(f"OpenAI error in chat: {e}") + + if not response_content and GROQ_API_KEY: + try: + response_content = get_groq_response(prompt, history=history, language="English") + except Exception as e: + print(f"Groq error in chat: {e}") + + if not response_content: + # Fallback to local Ollama integration + try: + response_content = get_ollama_response(prompt, history=history, language="English") + except Exception as e: + print(f"Ollama error in chat: {e}") + + if not response_content: + response_content = get_offline_chat_fallback(db, user_id, prompt) + + # Save conversation + chat_memory.add_message(user_id, "user", prompt) + chat_memory.add_message(user_id, "assistant", response_content) + + return response_content + +def stream_chat_response(db: Session, user_id: str, prompt: str): + """ + Generates streaming chunks for WebSocket or HTTP SSE. + """ + from app.ai.ollama_integration import has_active_ai_backend + + if not has_active_ai_backend(): + fallback_msg = get_offline_chat_fallback(db, user_id, prompt) + chat_memory.add_message(user_id, "user", prompt) + chat_memory.add_message(user_id, "assistant", fallback_msg) + # Yield words slowly to simulate streaming + import time + for word in fallback_msg.split(" "): + yield word + " " + time.sleep(0.05) + return + + sys_prompt = get_contextual_system_prompt(db, user_id) + history = chat_memory.get_history(user_id) + + full_messages = [{"role": "system", "content": sys_prompt}] + for msg in history: + full_messages.append({"role": msg["role"], "content": msg["content"]}) + full_messages.append({"role": "user", "content": prompt}) + + # Save user message to history + chat_memory.add_message(user_id, "user", prompt) + + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") + GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") + + complete_reply = "" + + if OPENAI_API_KEY: + try: + from openai import OpenAI + client = OpenAI(api_key=OPENAI_API_KEY) + stream = client.chat.completions.create( + model="gpt-4o-mini", + messages=full_messages, + temperature=0.1, + max_tokens=500, + stream=True + ) + for chunk in stream: + content = chunk.choices[0].delta.content + if content: + complete_reply += content + yield content + # Save assistant message once streaming completes + chat_memory.add_message(user_id, "assistant", complete_reply) + return + except Exception as e: + print(f"OpenAI streaming error: {e}") + + if GROQ_API_KEY: + try: + for chunk in stream_groq_response(prompt, history=history, language="English"): + if chunk: + complete_reply += chunk + yield chunk + chat_memory.add_message(user_id, "assistant", complete_reply) + return + except Exception as e: + print(f"Groq streaming error: {e}") + + # Fallback to local Ollama stream + try: + for chunk in stream_ollama_response(prompt, history=history, language="English"): + if chunk: + complete_reply += chunk + yield chunk + chat_memory.add_message(user_id, "assistant", complete_reply) + except Exception as e: + print(f"Ollama streaming error: {e}") + fallback_msg = get_offline_chat_fallback(db, user_id, prompt) + yield fallback_msg + chat_memory.add_message(user_id, "assistant", fallback_msg) diff --git a/backend/app/ai/coaching.py b/backend/app/ai/coaching.py new file mode 100644 index 0000000000000000000000000000000000000000..ef921b39d32a3101503e0505560a7d14bffbb5d8 --- /dev/null +++ b/backend/app/ai/coaching.py @@ -0,0 +1,244 @@ +import os +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from app.database.models import User, Account, Transaction, Goal, Investment, Subscription +from app.ai.forecasting import get_cashflow_metrics +from app.ai.ollama_integration import get_ai_response + +def calculate_financial_health_score(db: Session, user_id: str): + """ + Computes a multi-dimensional Financial Health Score (0-100) based on real database records. + """ + accounts = db.query(Account).filter(Account.user_id == user_id).all() + total_balance = sum(acc.balance for acc in accounts) + savings_balance = sum(acc.balance for acc in accounts if acc.type.lower() == "savings") + + # Cashflow + current_balance, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + monthly_income = max(1000.0, daily_income * 30.4) + monthly_spending = daily_spending * 30.4 + + # 1. Savings Consistency (20 pts) + # Check frequency of saving transactions or goal additions + txns = db.query(Transaction).join(Account).filter( + Account.user_id == user_id, + Transaction.type == "credit", + Transaction.category == "Income" + ).count() + # Let's say if they have active goals with current_amount > 0, they get higher points + goals = db.query(Goal).filter(Goal.user_id == user_id).all() + goal_savings = sum(g.current_amount for g in goals) + + savings_score = 10.0 + if goal_savings > 1000: + savings_score += 10.0 + elif goal_savings > 0: + savings_score += 5.0 + + # 2. Debt Ratio (20 pts) + # Estimate EMIs or goals with "debt" + debt_goals = sum(g.target_amount - g.current_amount for g in goals if "debt" in g.title.lower() or "loan" in g.title.lower()) + # Standard monthly debt service (estimate 10% of debt or $150 minimum if debt exists) + est_monthly_debt = max(0.0, debt_goals * 0.05) + debt_to_income = est_monthly_debt / monthly_income + + debt_score = 20.0 + if debt_to_income > 0.40: + debt_score = 5.0 + elif debt_to_income > 0.20: + debt_score = 12.0 + elif debt_to_income > 0.05: + debt_score = 18.0 + + # 3. Spending Discipline (20 pts) + # Ratio of monthly spending to monthly income + savings_rate = (monthly_income - monthly_spending) / monthly_income if monthly_income > 0 else 0 + discipline_score = 10.0 + if savings_rate >= 0.30: + discipline_score = 20.0 + elif savings_rate >= 0.15: + discipline_score = 16.0 + elif savings_rate >= 0.0: + discipline_score = 12.0 + + # 4. Emergency Fund (20 pts) + # Do they have 3-6 months of expenses in savings? + monthly_expenses = max(500.0, monthly_spending) + months_buffer = savings_balance / monthly_expenses + + emergency_score = 0.0 + if months_buffer >= 6.0: + emergency_score = 20.0 + elif months_buffer >= 3.0: + emergency_score = 15.0 + elif months_buffer >= 1.0: + emergency_score = 8.0 + + # 5. Investment Index (10 pts) + investments = db.query(Investment).filter(Investment.user_id == user_id).all() + inv_total = sum(i.current_value for i in investments) + + investment_score = 0.0 + if inv_total > 5000: + investment_score = 10.0 + elif inv_total > 0: + investment_score = 6.0 + + # 6. Subscription Efficiency (10 pts) + subs = db.query(Subscription).filter(Subscription.user_id == user_id, Subscription.active == True).all() + sub_cost = sum(s.amount if s.billing_cycle.lower() == "monthly" else (s.amount / 12) for s in subs) + sub_ratio = sub_cost / monthly_income + + sub_score = 10.0 + if sub_ratio > 0.10: # More than 10% of income on subscriptions + sub_score = 3.0 + elif sub_ratio > 0.05: # More than 5% + sub_score = 7.0 + + # Calculate Overall Score + overall_score = savings_score + debt_score + discipline_score + emergency_score + investment_score + sub_score + overall_score = min(100.0, max(0.0, overall_score)) + + # Actionable improvements list + improvements = [] + if savings_score < 15: + improvements.append("Set up automated transfers to your Savings account right after payday.") + if debt_score < 15: + improvements.append("Prioritize high-interest debt payoffs using the debt avalanche method.") + if discipline_score < 15: + improvements.append("Discretionary spending (shopping & dining) is high. Try implementing a $50 weekly limit.") + if emergency_score < 15: + improvements.append(f"Build savings buffer. Try to accumulate at least ${monthly_expenses * 3:,.2f} (3 months of expenses).") + if investment_score < 6: + improvements.append("Start a low-cost stock index fund SIP to counter inflation.") + if sub_score < 8: + improvements.append("Conduct an audit of active subscriptions. Cancel duplicate/unused memberships.") + + if not improvements: + improvements.append("Maintain your current financial habits; your portfolio is highly optimized!") + + # AI Explanation + user = db.query(User).filter(User.id == user_id).first() + persona = user.financial_personality if user else "Saver" + + ai_prompt = f""" + The user is a '{persona}' with a Financial Health Score of {overall_score:.0f}/100. + Sub-scores: + - Savings Consistency: {savings_score:.0f}/20 + - Debt Management: {debt_score:.0f}/20 + - Spending Discipline: {discipline_score:.0f}/20 + - Emergency Fund: {emergency_score:.0f}/20 + - Investment Allocation: {investment_score:.0f}/10 + - Subscription Management: {sub_score:.0f}/10 + + Write a concise, professional financial analyst explanation of this score. Detail the primary strengths and key weaknesses. + Do NOT write a generic chatbot reply. Keep it to 3-4 sentences. Format like a Bloomberg analyst report. + """ + + from app.ai.ollama_integration import has_active_ai_backend + + explanation = None + if has_active_ai_backend(): + try: + # Hard 8-second timeout so the health score endpoint never hangs + import threading + result = [None] + def _call(): + result[0] = get_ai_response(ai_prompt) + t = threading.Thread(target=_call, daemon=True) + t.start() + t.join(timeout=8) + explanation = result[0] + except Exception: + pass + + if not explanation: + explanation = f"As a {persona}, your financial health score of {overall_score:.0f} reflects solid fundamentals with opportunities to optimize emergency allocations and subscription efficiencies. Focus on automating savings and expanding investments." + + return { + "overall_score": round(overall_score, 0), + "categories": { + "savings_consistency": {"score": round(savings_score, 0), "max": 20}, + "debt_ratio": {"score": round(debt_score, 0), "max": 20}, + "spending_discipline": {"score": round(discipline_score, 0), "max": 20}, + "emergency_funds": {"score": round(emergency_score, 0), "max": 20}, + "investments": {"score": round(investment_score, 0), "max": 10}, + "subscription_management": {"score": round(sub_score, 0), "max": 10} + }, + "explanation": explanation, + "actionable_improvements": improvements + } + +def generate_daily_briefing(db: Session, user_id: str): + """ + Pulls complete financial context and generates a personalized daily financial briefing. + """ + user = db.query(User).filter(User.id == user_id).first() + if not user: + return {"briefing": "User not found."} + + # Collect data + accounts = db.query(Account).filter(Account.user_id == user_id).all() + total_balance = sum(acc.balance for acc in accounts) + + goals = db.query(Goal).filter(Goal.user_id == user_id).all() + goals_summary = [f"{g.title}: {g.current_amount}/{g.target_amount}" for g in goals] + + investments = db.query(Investment).filter(Investment.user_id == user_id).all() + inv_summary = [f"{i.asset_name} ({i.type}): Current Value ${i.current_value:,.2f}" for i in investments] + + # Cashflow + current_balance, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + monthly_income = daily_income * 30.4 + monthly_spending = daily_spending * 30.4 + + # Format AI Prompt + ai_prompt = f""" + You are an AI Wealth Advisor and Predictive Banking Engine. Generate a personalized daily financial briefing for {user.profile_data.get('name', 'User')}. + + Financial Summary: + - User Personality: {user.financial_personality} + - Total Account Balance: ${total_balance:,.2f} + - Estimated Monthly Income: ${monthly_income:,.2f} + - Estimated Monthly Spending: ${monthly_spending:,.2f} + - Active Goals: {', '.join(goals_summary) if goals_summary else 'None'} + - Investments: {', '.join(inv_summary) if inv_summary else 'None'} + + Generate a 3-paragraph daily briefing. + Paragraph 1: Summary of their current liquidity and portfolio health. + Paragraph 2: One specific recommendation regarding their savings goals or investment potential. + Paragraph 3: A behavioral spending insight warning based on their spending velocity. + + Style: Bloomberg Terminal style, highly intelligent, concise, financially meaningful, human-like. + Avoid boilerplate generic remarks (e.g. 'You should try saving more money'). Use exact figures. + """ + + from app.ai.ollama_integration import has_active_ai_backend + + briefing = None + if has_active_ai_backend(): + try: + import threading + result = [None] + def _call(): + result[0] = get_ai_response(ai_prompt) + t = threading.Thread(target=_call, daemon=True) + t.start() + t.join(timeout=10) + briefing = result[0] + except Exception: + pass + + if not briefing: + briefing = f"DAILY BRIEFING:\n\nYour liquid capital stands at ${total_balance:,.2f}. Portfolio indicators suggest regular cashflow velocity. Based on your {user.financial_personality} profile, we advise dedicating a portion of your net surplus to your active goals to optimize compound growth. Avoid non-essential weekend dining and retail spikes to maintain your target trajectory." + + return { + "date": datetime.utcnow().strftime("%Y-%m-%d"), + "user_name": user.profile_data.get('name', 'User'), + "briefing": briefing, + "metrics": { + "total_liquid_capital": round(total_balance, 2), + "monthly_income_projection": round(monthly_income, 2), + "monthly_burn_rate": round(monthly_spending, 2) + } + } diff --git a/backend/app/ai/forecasting.py b/backend/app/ai/forecasting.py new file mode 100644 index 0000000000000000000000000000000000000000..e8ef7ef3b78a3c30b632c5f0b45ccf6e76ce6133 --- /dev/null +++ b/backend/app/ai/forecasting.py @@ -0,0 +1,182 @@ +from datetime import datetime, timedelta +import numpy as np +from sqlalchemy.orm import Session +from app.database.models import Account, Transaction, Goal, Investment, Subscription + +def get_cashflow_metrics(db: Session, user_id: str, days: int = 90): + """ + Computes daily average income and spending based on historical transactions. + """ + # Fetch checking & savings accounts for user + accounts = db.query(Account).filter(Account.user_id == user_id).all() + account_ids = [acc.id for acc in accounts] + + if not account_ids: + return 0.0, 0.0, 0.0 # Current balance, avg daily income, avg daily spending + + current_balance = sum(acc.balance for acc in accounts) + + # Fetch recent transactions + cutoff = datetime.utcnow() - timedelta(days=days) + txns = db.query(Transaction).filter( + Transaction.account_id.in_(account_ids), + Transaction.timestamp >= cutoff + ).all() + + if not txns: + return current_balance, 0.0, 0.0 + + total_income = sum(t.amount for t in txns if t.type.lower() == "credit") + total_spending = sum(t.amount for t in txns if t.type.lower() == "debit") + + avg_daily_income = total_income / days + avg_daily_spending = total_spending / days + + return current_balance, avg_daily_income, avg_daily_spending + +def predict_future_balance(db: Session, user_id: str, projection_days: int = 90): + """ + Predicts future balances and returns trend description. + """ + current_balance, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + + net_daily = daily_income - daily_spending + projected_balance = max(0.0, current_balance + (net_daily * projection_days)) + + # Calculate percentage change + if current_balance > 0: + percent_change = (projected_balance - current_balance) / current_balance * 100 + else: + percent_change = 0.0 + + # Generate human-friendly description + if percent_change < 0: + insight = f"If current spending continues, your total balance may decrease by {abs(percent_change):.1f}% (down to ${projected_balance:,.2f}) in {projection_days} days." + elif percent_change > 0: + insight = f"Based on current trends, your total balance is projected to grow by {percent_change:.1f}% (up to ${projected_balance:,.2f}) in {projection_days} days." + else: + insight = "Your financial trajectory is steady with minor balance fluctuations." + + # Generate daily data points for charts + chart_data = [] + for day in range(0, projection_days + 1, 5): + val = max(0.0, current_balance + (net_daily * day)) + date_str = (datetime.utcnow() + timedelta(days=day)).strftime("%Y-%m-%d") + chart_data.append({"date": date_str, "balance": round(val, 2)}) + + return { + "current_balance": round(current_balance, 2), + "projected_balance": round(projected_balance, 2), + "percent_change": round(percent_change, 2), + "net_daily": round(net_daily, 2), + "insight": insight, + "chart_data": chart_data + } + +def forecast_savings_and_investments(db: Session, user_id: str, projection_months: int = 12): + """ + Projects savings and investment growth. + """ + accounts = db.query(Account).filter(Account.user_id == user_id).all() + savings_balance = sum(acc.balance for acc in accounts if acc.type.lower() == "savings") + checking_balance = sum(acc.balance for acc in accounts if acc.type.lower() == "checking") + + investments = db.query(Investment).filter(Investment.user_id == user_id).all() + total_invested = sum(inv.current_value for inv in investments) + + # Subscriptions and recurring bills + subs = db.query(Subscription).filter(Subscription.user_id == user_id, Subscription.active == True).all() + monthly_sub_cost = sum(sub.amount if sub.billing_cycle.lower() == "monthly" else (sub.amount / 12) for sub in subs) + + # Let's assume standard default rates if not specified + savings_apr = 0.04 # 4% APY + investment_apr = 0.08 # 8% APY + + # We assume the user saves 10% of their net income monthly (derived from transaction history) + _, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + monthly_income = daily_income * 30.4 + monthly_spending = daily_spending * 30.4 + net_monthly = max(0.0, monthly_income - monthly_spending) + monthly_savings_addition = net_monthly * 0.5 # Put 50% of net into savings + monthly_investment_addition = net_monthly * 0.3 # Put 30% of net into investments + + savings_data = [] + investment_data = [] + debt_data = [] + + current_savings = savings_balance + current_inv = total_invested + + # Let's model a baseline debt if the user has a Goal of type "debt" or a general dummy debt + # We will look for Goals containing "debt" or "loan" + goals = db.query(Goal).filter(Goal.user_id == user_id).all() + debt_goal = next((g for g in goals if "debt" in g.title.lower() or "loan" in g.title.lower()), None) + + total_debt = 5000.0 # Default initial simulated debt if none found + if debt_goal: + total_debt = max(0.0, debt_goal.target_amount - debt_goal.current_amount) + + monthly_debt_payment = min(total_debt, max(150.0, net_monthly * 0.1)) # Assume 10% of net or at least $150 + + for month in range(0, projection_months + 1): + # Compounding savings + if month > 0: + current_savings = (current_savings + monthly_savings_addition) * (1 + savings_apr / 12) + current_inv = (current_inv + monthly_investment_addition) * (1 + investment_apr / 12) + total_debt = max(0.0, total_debt - monthly_debt_payment) + + label = f"Month {month}" + savings_data.append({"month": label, "amount": round(current_savings, 2)}) + investment_data.append({"month": label, "amount": round(current_inv, 2)}) + debt_data.append({"month": label, "amount": round(total_debt, 2)}) + + return { + "projection_months": projection_months, + "monthly_savings_addition": round(monthly_savings_addition, 2), + "monthly_investment_addition": round(monthly_investment_addition, 2), + "savings_growth": savings_data, + "investment_growth": investment_data, + "debt_decline": debt_data, + "total_projected_savings": round(current_savings, 2), + "total_projected_investments": round(current_inv, 2), + "total_remaining_debt": round(total_debt, 2) + } + +def simulate_future_scenarios(db: Session, user_id: str, projection_months: int = 6): + """ + Simulates three scenarios: Status Quo, Frugal (cut spending 20%), and Luxury (increase spending 15%). + """ + current_balance, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + monthly_income = daily_income * 30.4 + monthly_spending = daily_spending * 30.4 + + scenarios = { + "status_quo": {"spend_mult": 1.0, "name": "Status Quo (Current spending)"}, + "frugal": {"spend_mult": 0.8, "name": "Frugal Mode (Cut non-essentials by 20%)"}, + "lifestyle_inflation": {"spend_mult": 1.15, "name": "Lifestyle Inflation (+15% spending)"} + } + + results = {} + for key, config in scenarios.items(): + mult = config["spend_mult"] + projected_spend = monthly_spending * mult + net_monthly = monthly_income - projected_spend + + balance_trend = [] + balance = current_balance + for m in range(0, projection_months + 1): + if m > 0: + balance = max(0.0, balance + net_monthly) + balance_trend.append({"month": f"M{m}", "balance": round(balance, 2)}) + + results[key] = { + "name": config["name"], + "monthly_income": round(monthly_income, 2), + "monthly_spending": round(projected_spend, 2), + "net_monthly": round(net_monthly, 2), + "balance_projection": balance_trend, + "final_balance": round(balance, 2), + "savings_change_pct": round(((balance - current_balance) / current_balance * 100) if current_balance > 0 else 0.0, 2) + } + + return results diff --git a/backend/app/ai/fraud.py b/backend/app/ai/fraud.py new file mode 100644 index 0000000000000000000000000000000000000000..8731781c1da67299cf0c50cc7f57039b4df6c5a3 --- /dev/null +++ b/backend/app/ai/fraud.py @@ -0,0 +1,123 @@ +from datetime import datetime, timedelta +import numpy as np +from sqlalchemy.orm import Session +from app.database.models import Transaction, FraudLog, Account, User + +def evaluate_transaction_for_fraud(db: Session, transaction_id: str): + """ + Evaluates a transaction for anomalies, generates a score, and logs alerts. + """ + txn = db.query(Transaction).filter(Transaction.id == transaction_id).first() + if not txn: + return {"error": "Transaction not found"} + + account = db.query(Account).filter(Account.id == txn.account_id).first() + if not account: + return {"error": "Account not found for transaction"} + + user_id = account.user_id + + # Fetch historical transactions to compare + history = db.query(Transaction).join(Account).filter( + Account.user_id == user_id, + Transaction.type == "debit", + Transaction.id != transaction_id + ).order_by(Transaction.timestamp.desc()).limit(30).all() + + score = 0 + reasons = [] + + # 1. Spikes in amount + if history: + amounts = [h.amount for h in history] + avg_amount = np.mean(amounts) + std_amount = np.std(amounts) if len(amounts) > 1 else 0.0 + + if txn.amount > avg_amount * 3.5: + score += 40 + reasons.append(f"Transaction amount (${txn.amount:,.2f}) is abnormally high compared to your historical average of ${avg_amount:,.2f}.") + elif txn.amount > avg_amount * 2.0: + score += 20 + reasons.append(f"Transaction amount is significantly higher than usual (2x historical average).") + else: + avg_amount = 0.0 + + # 2. Timing anomaly (Late night 11 PM - 4 AM) + hour = txn.timestamp.hour + if hour >= 23 or hour < 4: + score += 25 + reasons.append("Unusual timing (transaction placed between 11 PM and 4 AM).") + + # 3. Frequency anomaly (rapid consecutive transactions) + if history: + latest_txn = history[0] + time_diff = abs((txn.timestamp - latest_txn.timestamp).total_seconds()) + if time_diff < 180: # Less than 3 minutes + score += 20 + reasons.append("High-frequency activity: multiple transactions placed within 3 minutes.") + + # 4. Duplicate transaction check (same merchant and amount within 10 minutes) + if history: + for prev in history[:5]: + time_diff = abs((txn.timestamp - prev.timestamp).total_seconds()) + if prev.merchant == txn.merchant and prev.amount == txn.amount and time_diff < 600: + score += 30 + reasons.append(f"Potential duplicate payment: identical debit of ${txn.amount:.2f} at {txn.merchant} detected within 10 minutes.") + break + + # Normalize score to 100 max + score = min(100, score) + + # Log to DB if score exceeds threshold + if score >= 30: + # Check if fraud log already exists + existing_log = db.query(FraudLog).filter(FraudLog.transaction_id == txn.id).first() + if not existing_log: + fraud_log = FraudLog( + transaction_id=txn.id, + risk_score=score / 100.0, + suspicious_activity_details="; ".join(reasons), + status="pending" + ) + db.add(fraud_log) + db.commit() + + return { + "transaction_id": txn.id, + "amount": txn.amount, + "merchant": txn.merchant, + "timestamp": txn.timestamp.isoformat(), + "fraud_risk_score": score, + "is_anomalous": score >= 30, + "explanations": reasons, + "status": "flagged" if score >= 50 else "suspicious" if score >= 30 else "verified" + } + +def get_user_fraud_alerts(db: Session, user_id: str): + """ + Retrieves all flagged/suspicious transaction records and logs. + """ + logs = db.query(FraudLog).join(Transaction).join(Account).filter( + Account.user_id == user_id + ).order_by(Transaction.timestamp.desc()).all() + + alerts = [] + for log in logs: + txn = log.transaction + alerts.append({ + "fraud_log_id": log.id, + "transaction_id": txn.id, + "amount": txn.amount, + "merchant": txn.merchant, + "category": txn.category, + "timestamp": txn.timestamp.isoformat(), + "risk_score": round(log.risk_score * 100, 0), + "details": log.suspicious_activity_details, + "status": log.status + }) + + return { + "total_alerts": len(alerts), + "pending_reviews": sum(1 for a in alerts if a["status"] == "pending"), + "alerts": alerts + } diff --git a/backend/app/ai/fraud_detection.py b/backend/app/ai/fraud_detection.py new file mode 100644 index 0000000000000000000000000000000000000000..b5811e120602db35636e6fd3085cc8737a75097c --- /dev/null +++ b/backend/app/ai/fraud_detection.py @@ -0,0 +1,286 @@ +""" +AI Fraud Detection System for BankBot +Uses machine learning to detect suspicious transactions +""" + +import json +import os +import numpy as np +import pandas as pd +from datetime import datetime, timedelta +from sklearn.ensemble import IsolationForest, RandomForestClassifier +from sklearn.preprocessing import StandardScaler +import pickle +import uuid + +FRAUD_ALERTS_FILE = "fraud_alerts.json" +FRAUD_MODEL_FILE = "fraud_model.pkl" + +class FraudDetectionEngine: + """Advanced fraud detection using multiple ML algorithms""" + + def __init__(self): + self.isolation_forest = None + self.scaler = StandardScaler() + self.load_model() + + def load_model(self): + """Load saved model or create new one""" + if os.path.exists(FRAUD_MODEL_FILE): + try: + with open(FRAUD_MODEL_FILE, "rb") as f: + model_data = pickle.load(f) + self.isolation_forest = model_data.get("model") + self.scaler = model_data.get("scaler", StandardScaler()) + except Exception as e: + print(f"Error loading fraud model: {e}") + self._initialize_model() + else: + self._initialize_model() + + def _initialize_model(self): + """Initialize Isolation Forest for anomaly detection""" + self.isolation_forest = IsolationForest( + contamination=0.1, # Expect ~10% anomalies + random_state=42, + n_estimators=100 + ) + + def save_model(self): + """Save trained model to disk""" + try: + with open(FRAUD_MODEL_FILE, "wb") as f: + pickle.dump({ + "model": self.isolation_forest, + "scaler": self.scaler + }, f) + except Exception as e: + print(f"Error saving fraud model: {e}") + + def extract_features(self, transactions): + """ + Extract numerical features from transaction history + Returns: DataFrame with features for ML model + """ + if not transactions or len(transactions) < 2: + return None + + df = pd.DataFrame(transactions) + + # Convert date strings to datetime + df['date'] = pd.to_datetime(df['date'], errors='coerce') + + features = [] + for txn in transactions: + try: + amount = float(txn.get('amount', 0)) + txn_type = 1 if txn.get('type') == 'debit' else 0 + + feature_dict = { + 'amount': amount, + 'type': txn_type, + 'hour': datetime.fromisoformat(txn.get('date', '')).hour if txn.get('date') else 12, + 'day_of_week': datetime.fromisoformat(txn.get('date', '')).weekday() if txn.get('date') else 3, + } + features.append(feature_dict) + except Exception as e: + print(f"Error extracting features: {e}") + continue + + return pd.DataFrame(features) if features else None + + def detect_anomalies(self, transactions): + """ + Detect anomalous transactions using Isolation Forest + Returns: List of anomaly indices and scores + """ + if not transactions or len(transactions) < 2: + return [], [] + + features_df = self.extract_features(transactions) + if features_df is None or len(features_df) < 2: + return [], [] + + try: + # Normalize features + X = self.scaler.fit_transform(features_df) + + # Detect anomalies (-1 = anomaly, 1 = normal) + predictions = self.isolation_forest.predict(X) + anomaly_scores = self.isolation_forest.score_samples(X) + + # Find anomalies + anomalies = np.where(predictions == -1)[0].tolist() + + return anomalies, anomaly_scores + except Exception as e: + print(f"Error in anomaly detection: {e}") + return [], [] + + def calculate_fraud_score(self, transaction, user_history): + """ + Calculate fraud probability for a single transaction (0-100) + Considers: amount, frequency, location, time patterns + """ + score = 0 + reasons = [] + + try: + amount = float(transaction.get('amount', 0)) + + # Rule 1: Large withdrawal + avg_transaction = np.mean([float(t.get('amount', 0)) + for t in user_history[-20:] if t.get('type') == 'debit']) + if avg_transaction > 0 and amount > avg_transaction * 3: + score += 25 + reasons.append("Unusually large transaction") + + # Rule 2: Rapid consecutive transactions (within 5 minutes) + if len(user_history) > 1: + last_txn_time = datetime.fromisoformat(user_history[0].get('date', '')) + current_time = datetime.fromisoformat(transaction.get('date', '')) + if (current_time - last_txn_time).total_seconds() < 300: + score += 20 + reasons.append("Rapid consecutive transactions") + + # Rule 3: Late night transaction (11 PM - 4 AM) + try: + hour = datetime.fromisoformat(transaction.get('date', '')).hour + if hour >= 23 or hour < 4: + score += 15 + reasons.append("Unusual time of transaction") + except: + pass + + # Rule 4: Weekend large transaction + try: + day = datetime.fromisoformat(transaction.get('date', '')).weekday() + if day >= 5 and amount > avg_transaction * 2: # Saturday/Sunday + score += 10 + reasons.append("Weekend large transaction") + except: + pass + + # Rule 5: Debit after multiple recent debits + debit_count = sum(1 for t in user_history[-5:] if t.get('type') == 'debit') + if debit_count >= 4: + score += 15 + reasons.append("Multiple recent debits") + + # Cap score at 100 + score = min(score, 100) + + except Exception as e: + print(f"Error calculating fraud score: {e}") + + return score, reasons + +def check_fraud_alerts(username, users_data): + """ + Check for fraud alerts for a user + Returns: List of fraud alerts + """ + user_data = users_data.get(username, {}) + transactions = user_data.get('transactions', []) + + if not transactions: + return [] + + detector = FraudDetectionEngine() + alerts = [] + + try: + # Analyze recent transactions (last 10) + recent_txns = transactions[:10] + anomalies, scores = detector.detect_anomalies(recent_txns) + + # Create alerts for anomalies + for idx in anomalies: + if idx < len(recent_txns): + txn = recent_txns[idx] + fraud_score, reasons = detector.calculate_fraud_score(txn, recent_txns) + + if fraud_score > 30: # Alert threshold + alert = { + "id": str(uuid.uuid4()), + "transaction_id": txn.get('id'), + "amount": txn.get('amount'), + "fraud_score": fraud_score, + "reasons": reasons, + "timestamp": datetime.now().isoformat(), + "status": "active" + } + alerts.append(alert) + + except Exception as e: + print(f"Error checking fraud alerts: {e}") + + return alerts + +def get_fraud_alerts_summary(username, users_data): + """Get summary of fraud alerts for a user""" + alerts = check_fraud_alerts(username, users_data) + + high_risk = sum(1 for a in alerts if a.get('fraud_score', 0) > 70) + medium_risk = sum(1 for a in alerts if 30 < a.get('fraud_score', 0) <= 70) + + return { + "total_alerts": len(alerts), + "high_risk": high_risk, + "medium_risk": medium_risk, + "alerts": alerts[:5] # Return latest 5 + } + +def generate_fraud_report(username, users_data, days=30): + """Generate a comprehensive fraud analysis report""" + user_data = users_data.get(username, {}) + transactions = user_data.get('transactions', []) + + if not transactions: + return None + + # Filter transactions from last N days + cutoff_date = datetime.now() - timedelta(days=days) + recent_txns = [t for t in transactions + if datetime.fromisoformat(t.get('date', '')) > cutoff_date] + + detector = FraudDetectionEngine() + + # Calculate statistics + total_transactions = len(recent_txns) + total_debit = sum(float(t.get('amount', 0)) for t in recent_txns if t.get('type') == 'debit') + avg_transaction = total_debit / len([t for t in recent_txns if t.get('type') == 'debit']) if any(t.get('type') == 'debit' for t in recent_txns) else 0 + + # Run anomaly detection + anomalies, _ = detector.detect_anomalies(recent_txns) + + report = { + "period_days": days, + "total_transactions": total_transactions, + "total_debit_amount": total_debit, + "average_transaction": round(avg_transaction, 2), + "anomalies_detected": len(anomalies), + "risk_level": "HIGH" if len(anomalies) > total_transactions * 0.15 else "MEDIUM" if len(anomalies) > total_transactions * 0.05 else "LOW", + "alerts": check_fraud_alerts(username, users_data), + "recommendations": generate_fraud_recommendations(username, users_data) + } + + return report + +def generate_fraud_recommendations(username, users_data): + """Generate recommendations based on fraud analysis""" + alerts = check_fraud_alerts(username, users_data) + recommendations = [] + + if not alerts: + recommendations.append("✅ No suspicious activities detected. Your account is secure.") + else: + high_risk_count = sum(1 for a in alerts if a.get('fraud_score', 0) > 70) + if high_risk_count > 0: + recommendations.append(f"⚠️ {high_risk_count} high-risk transactions detected. Please verify them immediately.") + + recommendations.append("💡 Enable transaction alerts for amounts above ₹5,000") + recommendations.append("🔐 Review and update your password regularly") + recommendations.append("📱 Use 2FA for additional security") + + return recommendations diff --git a/backend/app/ai/loan_prediction_model.pkl b/backend/app/ai/loan_prediction_model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..cd4ea1f3207f62dba7fc6eba0683ce372fa11fb6 --- /dev/null +++ b/backend/app/ai/loan_prediction_model.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17a92ade8661126d286434ab96256939040736511018f552b2a2b78156a5377f +size 55529 diff --git a/backend/app/ai/loan_predictor.py b/backend/app/ai/loan_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..b143f986e5351f58bd1693bc817f555e41a1fc28 --- /dev/null +++ b/backend/app/ai/loan_predictor.py @@ -0,0 +1,301 @@ +""" +AI Loan Eligibility Predictor for BankBot +Predicts loan approval chance and EMI affordability using ML +""" + +import numpy as np +import pandas as pd +from sklearn.preprocessing import StandardScaler +from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier +import pickle +import os +from datetime import datetime +import json + +LOAN_MODEL_FILE = "loan_prediction_model.pkl" + +class LoanEligibilityPredictor: + """ML-based loan eligibility prediction""" + + def __init__(self): + self.classifier = None + self.scaler = StandardScaler() + self.feature_names = [ + 'salary', 'credit_score', 'existing_loans', + 'employment_years', 'age', 'loan_amount' + ] + self.load_model() + + def load_model(self): + """Load saved model or create new one""" + if os.path.exists(LOAN_MODEL_FILE): + try: + with open(LOAN_MODEL_FILE, "rb") as f: + model_data = pickle.load(f) + self.classifier = model_data.get("classifier") + self.scaler = model_data.get("scaler", StandardScaler()) + except Exception as e: + print(f"Error loading loan model: {e}") + self._initialize_model() + else: + self._initialize_model() + + def _initialize_model(self): + """Initialize Random Forest for loan prediction""" + # Create synthetic training data + X_train = np.array([ + [100000, 750, 0, 5, 35, 500000], # Approved + [150000, 800, 1, 10, 42, 1000000], # Approved + [200000, 780, 2, 8, 45, 1500000], # Approved + [50000, 600, 3, 2, 28, 300000], # Rejected + [80000, 650, 2, 3, 32, 400000], # Rejected + [120000, 700, 1, 6, 38, 600000], # Approved + [45000, 580, 4, 1, 25, 250000], # Rejected + [180000, 770, 0, 12, 50, 900000], # Approved + [70000, 620, 3, 2, 30, 350000], # Rejected + [160000, 790, 1, 9, 44, 800000], # Approved + ]) + + y_train = np.array([1, 1, 1, 0, 0, 1, 0, 1, 0, 1]) # 1=Approved, 0=Rejected + + # Normalize features + X_train_scaled = self.scaler.fit_transform(X_train) + + # Train classifier + self.classifier = RandomForestClassifier(n_estimators=100, random_state=42) + self.classifier.fit(X_train_scaled, y_train) + + self.save_model() + + def save_model(self): + """Save trained model to disk""" + try: + with open(LOAN_MODEL_FILE, "wb") as f: + pickle.dump({ + "classifier": self.classifier, + "scaler": self.scaler + }, f) + except Exception as e: + print(f"Error saving loan model: {e}") + + def predict_eligibility(self, salary, credit_score, existing_loans, + employment_years, age, loan_amount): + """ + Predict loan eligibility + Returns: Approval probability (0-100), risk level, recommendations + """ + try: + # Prepare features + features = np.array([[ + salary, credit_score, existing_loans, + employment_years, age, loan_amount + ]]) + + # Normalize + features_scaled = self.scaler.transform(features) + + # Predict probability + approval_prob = self.classifier.predict_proba(features_scaled)[0][1] * 100 + + # Calculate risk level + if approval_prob >= 80: + risk_level = "LOW RISK ✅" + elif approval_prob >= 60: + risk_level = "MEDIUM RISK ⚠️" + elif approval_prob >= 40: + risk_level = "HIGH RISK ❌" + else: + risk_level = "VERY HIGH RISK ❌" + + return approval_prob, risk_level + + except Exception as e: + print(f"Error in prediction: {e}") + return 50, "UNKNOWN RISK" + + def check_eligibility_rules(self, salary, credit_score, existing_loans, + employment_years, age, loan_amount): + """ + Check basic eligibility rules + Returns: Boolean and list of issues + """ + issues = [] + + # Age check + if age < 21: + issues.append("Age must be at least 21 years") + if age > 65: + issues.append("Age exceeds maximum limit (65 years)") + + # Employment check + if employment_years < 1: + issues.append("Minimum 1 year employment required") + + # Credit score check + if credit_score < 600: + issues.append("Credit score too low (minimum 600 required)") + + # Salary check + if salary < 25000: + issues.append("Salary too low for loan eligibility") + + # Loan amount vs salary ratio + emi_amount = calculate_emi(loan_amount, 12, 10) # Assume 12% rate, 10 years + if (emi_amount / salary) > 0.5: # EMI shouldn't exceed 50% of salary + issues.append(f"EMI of ₹{emi_amount:.2f} exceeds 50% of salary") + + # Existing loans check + if existing_loans > 3: + issues.append("Too many existing loans") + + is_eligible = len(issues) == 0 + return is_eligible, issues + + def calculate_loan_score(self, salary, credit_score, existing_loans, + employment_years, age, loan_amount): + """ + Calculate comprehensive loan score (0-100) + Considers multiple factors + """ + score = 0 + + # Credit score weight (40%) + credit_component = (min(credit_score, 850) / 850) * 40 + score += credit_component + + # Salary weight (30%) + salary_component = min((salary / 500000) * 30, 30) + score += salary_component + + # Employment years weight (15%) + employment_component = min((employment_years / 30) * 15, 15) + score += employment_component + + # Existing loans weight (10%) - negative impact + loan_penalty = min(existing_loans * 2, 10) + score -= loan_penalty + + # Age factor (5%) - younger is better + age_component = min(((65 - age) / 45) * 5, 5) + score += age_component + + # Loan affordability (penalties if high) + emi = calculate_emi(loan_amount, 12, 10) + if (emi / salary) > 0.5: + score -= 15 + elif (emi / salary) > 0.4: + score -= 10 + + return max(0, min(score, 100)) + +def calculate_emi(principal, rate_per_annum=10, years=10): + """ + Calculate EMI (Equated Monthly Installment) + Formula: EMI = P * r * (1+r)^n / ((1+r)^n - 1) + """ + monthly_rate = rate_per_annum / 100 / 12 + months = years * 12 + + if monthly_rate == 0: + return principal / months + + emi = principal * monthly_rate * ((1 + monthly_rate) ** months) / ( + ((1 + monthly_rate) ** months) - 1 + ) + return emi + +def calculate_loan_eligibility(salary, credit_score, existing_loans, + employment_years, age, loan_amount): + """Main function to calculate loan eligibility""" + predictor = LoanEligibilityPredictor() + + # Check basic eligibility + is_eligible, issues = predictor.check_eligibility_rules( + salary, credit_score, existing_loans, employment_years, age, loan_amount + ) + + # Get ML prediction + approval_prob, risk_level = predictor.predict_eligibility( + salary, credit_score, existing_loans, employment_years, age, loan_amount + ) + + # Calculate loan score + loan_score = predictor.calculate_loan_score( + salary, credit_score, existing_loans, employment_years, age, loan_amount + ) + + # Calculate EMI + emi = calculate_emi(loan_amount, 12, 10) + + # Get recommendations + recommendations = get_loan_recommendations( + approval_prob, salary, credit_score, existing_loans, employment_years, emi + ) + + result = { + "approval_probability": round(approval_prob, 1), + "approval_status": "APPROVED ✅" if approval_prob >= 60 else "REJECTED ❌" if approval_prob < 40 else "UNDER REVIEW ⏳", + "risk_level": risk_level, + "loan_score": round(loan_score, 1), + "is_rule_eligible": is_eligible, + "issues": issues, + "emi": round(emi, 2), + "total_amount": round(loan_amount + (emi * 12 * 10) - loan_amount, 2), + "monthly_emi": round(emi, 2), + "tenure_years": 10, + "rate_per_annum": 12, + "recommendations": recommendations + } + + return result + +def get_loan_recommendations(approval_prob, salary, credit_score, + existing_loans, employment_years, emi): + """Generate personalized loan recommendations""" + recommendations = [] + + if approval_prob >= 80: + recommendations.append("✅ You are likely to get approved for this loan amount") + elif approval_prob < 40: + recommendations.append("❌ Your approval chances are low. Consider these options:") + + if credit_score < 700: + recommendations.append(" • Improve your credit score to 700+") + + if existing_loans > 2: + recommendations.append(" • Pay off existing loans to improve your profile") + + recommendations.append(" • Apply for a smaller loan amount") + recommendations.append(" • Increase your employment tenure") + + else: + recommendations.append("⏳ Your application will be under review") + + # EMI affordability + emi_ratio = (emi / salary) * 100 + if emi_ratio > 50: + recommendations.append(f"⚠️ Your EMI (₹{emi:.2f}) is {emi_ratio:.1f}% of salary. Consider reducing loan amount.") + elif emi_ratio < 30: + recommendations.append(f"✅ Your EMI to salary ratio ({emi_ratio:.1f}%) is very healthy") + + return recommendations + +def generate_loan_comparison(loan_amount, rates=[9, 10, 11, 12, 13], tenure_years=[5, 7, 10]): + """Generate EMI comparison for different rates and tenures""" + comparison_data = [] + + for rate in rates: + for tenure in tenure_years: + emi = calculate_emi(loan_amount, rate, tenure) + total_amount = (emi * 12 * tenure) + interest = total_amount - loan_amount + + comparison_data.append({ + "rate": f"{rate}%", + "tenure": f"{tenure} years", + "emi": round(emi, 2), + "total_amount": round(total_amount, 2), + "interest": round(interest, 2) + }) + + return comparison_data diff --git a/backend/app/ai/ollama_integration.py b/backend/app/ai/ollama_integration.py new file mode 100644 index 0000000000000000000000000000000000000000..bf8056cb0cffaccf8269ef7149deabfa04329e87 --- /dev/null +++ b/backend/app/ai/ollama_integration.py @@ -0,0 +1,369 @@ +import os +import requests +import json +import time + +# ─── Backend credentials (read once at module load) ─────────────────────────── +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") +OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") +GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") +USE_GROQ = bool(GROQ_API_KEY) + +OLLAMA_URL = "http://127.0.0.1:11434" + +# Check active backends once at load time to prevent timeout delays during requests. +# Priority: OpenAI → Groq → local Ollama +AI_BACKEND_AVAILABLE = False +if OPENAI_API_KEY or GROQ_API_KEY: + AI_BACKEND_AVAILABLE = True +else: + try: + # Fast 0.5s ping to local Ollama + response = requests.get(f"{OLLAMA_URL}/", timeout=0.5) + AI_BACKEND_AVAILABLE = (response.status_code == 200) + except Exception: + AI_BACKEND_AVAILABLE = False + +def has_active_ai_backend() -> bool: + """Returns True if OpenAI, Groq, or local Ollama is active and reachable.""" + return AI_BACKEND_AVAILABLE + +BANKING_KEYWORDS = [ + "account", "loan", "card", "balance", + "transfer", "bank", "interest", "emi", + "credit", "debit", "kyc", "upi", "cheque", + "deposit", "fd", "rd", "branch", "ifsc", + "transaction", "payment", "savings", "checking", + "mortgage", "investment", "fintech", "wallet", + "rate", "rates", "support", "customer", "care", + "help", "contact", "helpline", "number", "call", + "document", "required", "identity", "proof", "open" +] + +SYSTEM_PROMPT = """You are BankBot, a professional banking assistant for Central Bank. +You ONLY answer banking-related questions. If the question is not related to banking, politely refuse. +Never answer questions about politics, sports, entertainment, coding, or personal advice. + +CORE GUIDELINES: +1. ALWAYS communicate in {language}. +2. CONTEXT AWARENESS: Use the provided chat history to understand follow-up questions. For example, if the user asks "What is the interest rate?" and then "for home loan", you must understand they are asking about home loan interest rates. +3. CLARIFYING QUESTIONS: If a user's query is ambiguous (e.g., "how much?"), ask for missing details (e.g., "How much for what service? Balance check or loan EMI?"). +4. CALCULATIONS: Perform financial calculations (EMI, Interest, Eligibility) if information is provided. +5. DOCUMENT ANALYSIS: If text from a PDF statement is provided, summarize it or answer specific questions about it. +6. PROFESSIONALISM: Maintain a helpful, formal, and secure tone.""" + +OLLAMA_URL = "http://127.0.0.1:11434" +DEFAULT_OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3:latest") + + +def is_banking_query(user_input): + input_lower = user_input.lower() + return any(word in input_lower for word in BANKING_KEYWORDS) + + +def get_active_backend(): + """Returns the highest-priority available backend name.""" + if OPENAI_API_KEY: + return "openai" + if USE_GROQ: + return "groq" + return "ollama" + + +def _build_messages(prompt, history=None, language="English"): + sys_prompt = SYSTEM_PROMPT.format(language=language) + messages = [{"role": "system", "content": sys_prompt}] + + if history: + for msg in history[-10:]: + if msg.get("role") and msg.get("content"): + messages.append({"role": msg["role"], "content": msg["content"]}) + + messages.append({"role": "user", "content": prompt}) + return messages + + +def _get_available_ollama_models(): + try: + response = requests.get(f"{OLLAMA_URL}/api/tags", timeout=5) + response.raise_for_status() + data = response.json() + return [model.get("name", "") for model in data.get("models", []) if model.get("name")] + except Exception as e: + print(f"Ollama model discovery error: {e}") + return [] + + +def _resolve_ollama_model(requested_model): + available_models = _get_available_ollama_models() + if not available_models: + return requested_model + + if requested_model in available_models: + return requested_model + + base_requested_model = requested_model.split(":", 1)[0] + for candidate in available_models: + if candidate.split(":", 1)[0] == base_requested_model: + return candidate + + return available_models[0] + + +def _ollama_error_message(model, error): + return ( + f"Ollama request failed for model '{model}': {error}. " + "The Ollama server is reachable, but the model backend crashed internally. " + "Try `ollama run llama3`, and if that fails restart Ollama with " + "`taskkill /F /IM ollama.exe` followed by `ollama serve`." + ) + + +# ─── OpenAI Functions ──────────────────────────────────────────────────────── + +def get_openai_response(prompt, history=None, model=None, language="English"): + """Fetches a response from the OpenAI API (gpt-4o-mini by default).""" + if not OPENAI_API_KEY: + return None + try: + from openai import OpenAI + client = OpenAI(api_key=OPENAI_API_KEY) + target_model = model or OPENAI_MODEL + + sys_prompt = SYSTEM_PROMPT.format(language=language) + messages = [{"role": "system", "content": sys_prompt}] + + if history: + for msg in history[-10:]: + if msg.get("role") and msg.get("content"): + messages.append({"role": msg["role"], "content": msg["content"]}) + + messages.append({"role": "user", "content": prompt}) + + response = client.chat.completions.create( + model=target_model, + messages=messages, + temperature=0.1, + max_tokens=1000, + ) + return response.choices[0].message.content + except Exception as e: + print(f"OpenAI Error: {e}") + return None + + +def stream_openai_response(prompt, history=None, model=None, language="English"): + """Yields streamed response chunks from the OpenAI API.""" + if not OPENAI_API_KEY: + return + try: + from openai import OpenAI + client = OpenAI(api_key=OPENAI_API_KEY) + target_model = model or OPENAI_MODEL + + sys_prompt = SYSTEM_PROMPT.format(language=language) + messages = [{"role": "system", "content": sys_prompt}] + + if history: + for msg in history[-10:]: + if msg.get("role") and msg.get("content"): + messages.append({"role": msg["role"], "content": msg["content"]}) + + messages.append({"role": "user", "content": prompt}) + + stream = client.chat.completions.create( + model=target_model, + messages=messages, + temperature=0.1, + max_tokens=1000, + stream=True, + ) + for chunk in stream: + content = chunk.choices[0].delta.content + if content: + yield content + except Exception as e: + print(f"OpenAI Stream Error: {e}") + + +# ─── Groq AI Functions ──────────────────────────────────────────────────────── + +def get_groq_response(prompt, history=None, model="llama-3.3-70b-versatile", language="English"): + """Fetches a response from Groq AI API.""" + try: + from groq import Groq + client = Groq(api_key=GROQ_API_KEY) + + sys_prompt = SYSTEM_PROMPT.format(language=language) + messages = [{"role": "system", "content": sys_prompt}] + + if history: + for msg in history[-10:]: + if msg.get("role") and msg.get("content"): + messages.append({"role": msg["role"], "content": msg["content"]}) + + messages.append({"role": "user", "content": prompt}) + + response = client.chat.completions.create( + model=model, + messages=messages, + temperature=0.1, + max_tokens=1000, + ) + return response.choices[0].message.content + except Exception as e: + print(f"Groq Error: {e}") + return None + + +def stream_groq_response(prompt, history=None, model="llama-3.3-70b-versatile", language="English"): + """Yields streamed response chunks from Groq AI API.""" + try: + from groq import Groq + client = Groq(api_key=GROQ_API_KEY) + + sys_prompt = SYSTEM_PROMPT.format(language=language) + messages = [{"role": "system", "content": sys_prompt}] + + if history: + for msg in history[-10:]: + if msg.get("role") and msg.get("content"): + messages.append({"role": msg["role"], "content": msg["content"]}) + + messages.append({"role": "user", "content": prompt}) + + stream = client.chat.completions.create( + model=model, + messages=messages, + temperature=0.1, + max_tokens=1000, + stream=True, + ) + for chunk in stream: + content = chunk.choices[0].delta.content + if content: + yield content + except Exception as e: + print(f"Groq Stream Error: {e}") + yield None + + +# ─── Ollama Functions ───────────────────────────────────────────────────────── + +def get_ollama_response(prompt, history=None, model=DEFAULT_OLLAMA_MODEL, language="English"): + """Fetches a response from a local Ollama instance.""" + url = f"{OLLAMA_URL}/api/chat" + resolved_model = _resolve_ollama_model(model) + messages = _build_messages(prompt, history=history, language=language) + + payload = { + "model": resolved_model, + "messages": messages, + "stream": False, + "options": {"temperature": 0.1, "top_p": 0.9, "num_predict": 500} + } + + try: + # (connect_timeout, read_timeout) — cap total generation at 25s + response = requests.post(url, json=payload, timeout=(5, 25)) + response.raise_for_status() + data = response.json() + return data.get("message", {}).get("content", "") + except requests.exceptions.Timeout: + # Don't retry on timeout — let the caller fall back to the next backend + print(f"Ollama timed out for model '{resolved_model}'. Falling back to next backend.") + return None + except Exception as e: + print(_ollama_error_message(resolved_model, e)) + if resolved_model != "llama3": + return get_ollama_response(prompt, history, model="llama3", language=language) + return None + + +def stream_ollama_response(prompt, history=None, model=DEFAULT_OLLAMA_MODEL, language="English"): + """Yields chunks of the response from a local Ollama instance for streaming.""" + url = f"{OLLAMA_URL}/api/chat" + resolved_model = _resolve_ollama_model(model) + messages = _build_messages(prompt, history=history, language=language) + + payload = { + "model": resolved_model, + "messages": messages, + "stream": True, + "options": {"temperature": 0.1, "top_p": 0.9, "num_predict": 500} + } + + try: + # (connect_timeout, read_timeout) — cap total generation at 25s + response = requests.post(url, json=payload, timeout=(5, 25), stream=True) + response.raise_for_status() + + for line in response.iter_lines(): + if line: + chunk = json.loads(line) + if 'message' in chunk and 'content' in chunk['message']: + yield chunk['message']['content'] + if chunk.get('done'): + break + except requests.exceptions.Timeout: + # Don't retry on timeout — let the caller fall back to the next backend + print(f"Ollama stream timed out for model '{resolved_model}'. Falling back to next backend.") + return + except Exception as e: + print(_ollama_error_message(resolved_model, e)) + if resolved_model != "llama3": + yield from stream_ollama_response(prompt, history, model="llama3", language=language) + else: + yield None + + +# ─── Unified Wrapper Functions ──────────────────────────────────────────────── + +def get_ai_response(prompt, history=None, language="English"): + """ + Auto-selects the best available backend. + Priority: OpenAI → Groq → Ollama + Returns None only when all backends are unavailable. + """ + if OPENAI_API_KEY: + result = get_openai_response(prompt, history, language=language) + if result: + return result + + if USE_GROQ: + result = get_groq_response(prompt, history, language=language) + if result: + return result + + return get_ollama_response(prompt, history, language=language) + + +def stream_ai_response(prompt, history=None, language="English"): + """ + Auto-selects streaming from the best available backend. + Priority: OpenAI → Groq → Ollama + """ + if OPENAI_API_KEY: + chunks = list(stream_openai_response(prompt, history, language=language)) + if chunks: + yield from chunks + return + + if USE_GROQ: + chunks = list(stream_groq_response(prompt, history, language=language)) + if chunks: + yield from chunks + return + + yield from stream_ollama_response(prompt, history, language=language) + + +def check_ollama_connection(): + """Checks if the local Ollama server is running.""" + if USE_GROQ: + return True + try: + response = requests.get(f"{OLLAMA_URL}/", timeout=2) + return response.status_code == 200 + except: + return False diff --git a/backend/app/ai/router.py b/backend/app/ai/router.py new file mode 100644 index 0000000000000000000000000000000000000000..a14a36488d5af93c1bfaa0fc2d88ad02b3052682 --- /dev/null +++ b/backend/app/ai/router.py @@ -0,0 +1,181 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import List, Optional + +from app.database.database import get_db +from app.database.models import User +from app.middleware.cache import cache + +# Import AI helper engines +from app.ai.forecasting import predict_future_balance, forecast_savings_and_investments, simulate_future_scenarios +from app.ai.simulation import simulate_purchase_impact, simulate_investment_impact, simulate_subscription_cancellation +from app.ai.behavior import analyze_spending_behavior +from app.ai.coaching import calculate_financial_health_score, generate_daily_briefing +from app.ai.subscriptions import analyze_subscriptions +from app.ai.fraud import evaluate_transaction_for_fraud, get_user_fraud_alerts +from app.ai.chat import get_chat_response + +router = APIRouter(prefix="/api/ai", tags=["AI Intelligence"]) + +# Fallback helper to retrieve a valid user ID for demonstration +def get_user_id_fallback(db: Session, user_id: Optional[str] = None) -> str: + if user_id: + return user_id + user = db.query(User).first() + if not user: + raise HTTPException(status_code=404, detail="No users found in database. Please seed the database first.") + return user.id + +# Pydantic Schemas for input +class PurchaseRequest(BaseModel): + amount: float + merchant: str + category: str + +class InvestmentRequest(BaseModel): + monthly_sip: float + asset_type: str + lump_sum: float = 0.0 + +class SubscriptionSimulationRequest(BaseModel): + subscription_ids: List[str] + +class ChatMessageRequest(BaseModel): + message: str + +# ─── FINANCIAL TWIN FORECASTS ────────────────────────────────────────────────── + +@router.get("/twin/predict") +def get_twin_predict(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + cache_key = f"ai:twin:predict:{uid}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = predict_future_balance(db, uid) + cache.set(cache_key, result, ttl=300) # cache for 5 minutes + return result + +@router.get("/twin/future") +def get_twin_future(user_id: Optional[str] = None, months: int = Query(default=12, ge=1, le=60), db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + cache_key = f"ai:twin:future:{uid}:{months}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = forecast_savings_and_investments(db, uid, months) + cache.set(cache_key, result, ttl=300) + return result + +@router.get("/twin/scenarios") +def get_twin_scenarios(user_id: Optional[str] = None, months: int = Query(default=6, ge=1, le=24), db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + cache_key = f"ai:twin:scenarios:{uid}:{months}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = simulate_future_scenarios(db, uid, months) + cache.set(cache_key, result, ttl=300) + return result + +# ─── SIMULATION ENDPOINTS ────────────────────────────────────────────────────── + +@router.post("/simulate/purchase") +def post_simulate_purchase(req: PurchaseRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + return simulate_purchase_impact(db, uid, req.amount, req.category, req.merchant) + +@router.post("/simulate/investment") +def post_simulate_investment(req: InvestmentRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + return simulate_investment_impact(db, uid, req.monthly_sip, req.asset_type, req.lump_sum) + +@router.post("/simulate/subscription") +def post_simulate_subscription(req: SubscriptionSimulationRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + return simulate_subscription_cancellation(db, uid, req.subscription_ids) + +# ─── BEHAVIORAL ANALYTICS ───────────────────────────────────────────────────── + +@router.get("/behavior/insights") +def get_behavior_insights(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + cache_key = f"ai:behavior:insights:{uid}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = analyze_spending_behavior(db, uid) + cache.set(cache_key, result, ttl=600) # cache for 10 minutes + return result + +# ─── COACHING & BRIEFINGS ───────────────────────────────────────────────────── + +@router.get("/coaching/briefing") +def get_coaching_briefing(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + # Cache briefings for 1 hour to prevent excessive LLM costs + cache_key = f"ai:coaching:briefing:{uid}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = generate_daily_briefing(db, uid) + cache.set(cache_key, result, ttl=3600) + return result + +@router.get("/coaching/score") +def get_coaching_score(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + cache_key = f"ai:coaching:score:{uid}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = calculate_financial_health_score(db, uid) + cache.set(cache_key, result, ttl=600) + return result + +# ─── SUBSCRIPTION OPTIMIZATION ──────────────────────────────────────────────── + +@router.get("/subscriptions/optimize") +def get_subscriptions_optimize(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + cache_key = f"ai:subs:optimize:{uid}" + + cached = cache.get(cache_key) + if cached: + return cached + + result = analyze_subscriptions(db, uid) + cache.set(cache_key, result, ttl=600) + return result + +# ─── FRAUD & SECURITY ───────────────────────────────────────────────────────── + +@router.get("/fraud/analysis") +def get_fraud_analysis(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + return get_user_fraud_alerts(db, uid) + +@router.post("/fraud/evaluate/{transaction_id}") +def post_fraud_evaluate(transaction_id: str, db: Session = Depends(get_db)): + return evaluate_transaction_for_fraud(db, transaction_id) + +# ─── CONTEXTUAL CHAT ENDPOINT ────────────────────────────────────────────────── + +@router.post("/chat") +def post_chat(req: ChatMessageRequest, user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = get_user_id_fallback(db, user_id) + response_msg = get_chat_response(db, uid, req.message) + return {"response": response_msg} diff --git a/backend/app/ai/simulation.py b/backend/app/ai/simulation.py new file mode 100644 index 0000000000000000000000000000000000000000..febc54ee0684fbf751b4f285fb3c5037952cc6ea --- /dev/null +++ b/backend/app/ai/simulation.py @@ -0,0 +1,204 @@ +from sqlalchemy.orm import Session +from app.database.models import Account, Goal, Investment, Subscription +from app.ai.forecasting import get_cashflow_metrics + +def simulate_purchase_impact(db: Session, user_id: str, amount: float, category: str, merchant: str): + """ + Simulates buying a large asset or item (e.g. a car) and assesses risk. + """ + accounts = db.query(Account).filter(Account.user_id == user_id).all() + total_balance = sum(acc.balance for acc in accounts) + checking_acc = next((a for a in accounts if a.type.lower() == "checking"), None) + + # Target emergency fund amount + goals = db.query(Goal).filter(Goal.user_id == user_id).all() + emergency_goal = next((g for g in goals if "emergency" in g.title.lower()), None) + emergency_threshold = emergency_goal.target_amount if emergency_goal else 3000.0 + + new_balance = total_balance - amount + + # Cashflow metrics + _, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + monthly_net = (daily_income - daily_spending) * 30.4 + + # Risk Analysis + risk_level = "low" + reasons = [] + + if amount > total_balance: + risk_level = "critical" + reasons.append("Purchase exceeds your total available balance, requiring debt.") + elif new_balance < emergency_threshold: + risk_level = "high" + reasons.append(f"This purchase depletes your emergency buffer (threshold of ${emergency_threshold:,.2f}).") + elif amount > total_balance * 0.3: + risk_level = "medium" + reasons.append("Single purchase consumes more than 30% of your total liquid cash.") + + if monthly_net < 0 and amount > 500: + risk_level = "high" + reasons.append("You have a negative monthly cashflow; making large purchases increases financial strain.") + + # Recommendations + recommendation = "" + if risk_level == "critical": + recommendation = "❌ Strongly advise against this purchase. Consider financing options, delaying, or establishing a dedicated goal." + elif risk_level == "high": + recommendation = "⚠️ Refrain from this purchase if possible. Rebuilding your emergency fund should be prioritized." + elif risk_level == "medium": + recommendation = "💡 Proceed with caution. Consider trimming discretionary expenses next month to offset the cost." + else: + recommendation = "✅ Purchase is safe. It fits within your financial profile without impacting key safety buffers." + + return { + "purchase_amount": amount, + "merchant": merchant, + "category": category, + "current_balance": round(total_balance, 2), + "projected_balance": round(max(0.0, new_balance), 2), + "savings_impact": { + "immediate_reduction": round(amount, 2), + "emergency_buffer_breached": new_balance < emergency_threshold, + "emergency_threshold": round(emergency_threshold, 2) + }, + "risk_analysis": { + "risk_level": risk_level, + "reasons": reasons + }, + "recommendation": recommendation + } + +def simulate_investment_impact(db: Session, user_id: str, monthly_sip: float, asset_type: str, lump_sum: float = 0.0): + """ + Simulates investment growth and evaluates opportunity cost. + """ + # Expected annual returns based on asset type + returns_map = { + "stock": 0.10, # 10% + "crypto": 0.20, # 20% + "mutual_fund": 0.08, # 8% + "fd": 0.05, # 5% + "bond": 0.04 # 4% + } + apr = returns_map.get(asset_type.lower(), 0.07) + + # Calculate current balance + accounts = db.query(Account).filter(Account.user_id == user_id).all() + total_balance = sum(acc.balance for acc in accounts) + + # Cashflow metrics + _, daily_income, daily_spending = get_cashflow_metrics(db, user_id) + monthly_net = (daily_income - daily_spending) * 30.4 + + # Check if SIP is affordable + is_affordable = monthly_net >= monthly_sip + + growth_projection = [] + current_value = lump_sum + total_invested = lump_sum + + # 5-year monthly projection + for month in range(0, 61): + if month > 0: + current_value = (current_value + monthly_sip) * (1 + apr / 12) + total_invested += monthly_sip + + if month in [12, 36, 60]: # Save 1, 3, 5 year markers + growth_projection.append({ + "year": month // 12, + "total_invested": round(total_invested, 2), + "future_value": round(current_value, 2), + "earnings": round(max(0.0, current_value - total_invested), 2) + }) + + risk_level = "low" + if asset_type.lower() == "crypto": + risk_level = "high" + elif asset_type.lower() in ["stock", "mutual_fund"] and monthly_sip > monthly_net * 0.5: + risk_level = "medium" + + recommendation = "" + if not is_affordable: + recommendation = f"⚠️ Your monthly net surplus (${monthly_net:,.2f}) is lower than the planned SIP (${monthly_sip:,.2f}). This may lead to checking overdrafts." + else: + recommendation = f"✅ Excellent choice. Investing ${monthly_sip:,.2f} monthly in {asset_type} is fully supported by your net cashflow." + + return { + "asset_type": asset_type, + "monthly_sip": round(monthly_sip, 2), + "lump_sum": round(lump_sum, 2), + "is_affordable": is_affordable, + "growth_projection": growth_projection, + "risk_analysis": { + "risk_level": risk_level, + "expected_annual_return": apr + }, + "savings_impact": { + "opportunity_cost_yearly": round(monthly_sip * 12, 2), + "monthly_surplus_retaining": round(max(0.0, monthly_net - monthly_sip), 2) + }, + "recommendation": recommendation + } + +def simulate_subscription_cancellation(db: Session, user_id: str, subscription_ids: list): + """ + Simulates the financial benefit of cancelling one or more subscriptions. + """ + subs = db.query(Subscription).filter( + Subscription.user_id == user_id, + Subscription.id.in_(subscription_ids) + ).all() + + if not subs: + return {"message": "No matching subscriptions found for cancellation simulation."} + + monthly_savings = 0.0 + yearly_savings = 0.0 + cancelled_details = [] + + for sub in subs: + cost = sub.amount + is_monthly = sub.billing_cycle.lower() == "monthly" + + m_cost = cost if is_monthly else (cost / 12) + y_cost = (cost * 12) if is_monthly else cost + + monthly_savings += m_cost + yearly_savings += y_cost + + cancelled_details.append({ + "id": sub.id, + "merchant": sub.merchant, + "amount": sub.amount, + "billing_cycle": sub.billing_cycle + }) + + # Relate savings to user's Goals + goals = db.query(Goal).filter(Goal.user_id == user_id).all() + first_goal = goals[0] if goals else None + + goal_impact = None + if first_goal: + months_saved = 0.0 + remaining_needed = max(0.0, first_goal.target_amount - first_goal.current_amount) + if monthly_savings > 0: + months_saved = remaining_needed / (remaining_needed / 12 if remaining_needed > 0 else 1) # simple logic + # Let's say if they direct this money to goal, it reduces target time by: + months_saved = (remaining_needed / monthly_savings) if remaining_needed > 0 else 0 + + goal_impact = { + "goal_title": first_goal.title, + "target_amount": round(first_goal.target_amount, 2), + "months_to_reach_with_savings": round(months_saved, 1) if monthly_savings > 0 else 0 + } + + # Recommendations + recommendation = f"Cancelling these subscriptions yields ${monthly_savings:,.2f} per month (${yearly_savings:,.2f} annually). Reinvesting these funds into high-yield savings or mutual funds is recommended." + + return { + "cancelled_subscriptions": cancelled_details, + "monthly_savings": round(monthly_savings, 2), + "yearly_savings": round(yearly_savings, 2), + "goal_impact": goal_impact, + "recommendation": recommendation + } diff --git a/backend/app/ai/subscriptions.py b/backend/app/ai/subscriptions.py new file mode 100644 index 0000000000000000000000000000000000000000..480e39a54493135431e327bb27fd12faff8d9ec3 --- /dev/null +++ b/backend/app/ai/subscriptions.py @@ -0,0 +1,105 @@ +from collections import defaultdict +from sqlalchemy.orm import Session +from app.database.models import Subscription + +def analyze_subscriptions(db: Session, user_id: str): + """ + Analyzes subscriptions to detect duplicates, unused memberships, and cancellation opportunities. + """ + subs = db.query(Subscription).filter( + Subscription.user_id == user_id, + Subscription.active == True + ).all() + + if not subs: + return { + "subscriptions": [], + "duplicates": [], + "unused_subscriptions": [], + "yearly_savings_potential": 0.0, + "risk_analysis": [] + } + + merchant_map = defaultdict(list) + unused_list = [] + cancellation_suggestions = [] + yearly_savings = 0.0 + risk_analysis = [] + + for s in subs: + # Standardize merchant name to detect duplicates + clean_merchant = s.merchant.strip().lower() + merchant_map[clean_merchant].append(s) + + # Determine cost + m_cost = s.amount if s.billing_cycle.lower() == "monthly" else (s.amount / 12) + y_cost = (s.amount * 12) if s.billing_cycle.lower() == "monthly" else s.amount + + # Detect unused (if frequency is 'low' or 'none' in usage detection metadata) + usage = s.ai_usage_detection or {} + freq = str(usage.get("usage_frequency", "medium")).lower() + + if freq in ["low", "none", "unused"]: + unused_list.append(s) + cancellation_suggestions.append({ + "subscription_id": s.id, + "merchant": s.merchant, + "amount": s.amount, + "billing_cycle": s.billing_cycle, + "reason": f"Usage frequency is flagged as '{freq}'.", + "yearly_savings": round(y_cost, 2) + }) + yearly_savings += y_cost + + # Detect duplicates + duplicates = [] + for merchant, items in merchant_map.items(): + if len(items) > 1: + total_cost = sum(x.amount for x in items) + duplicates.append({ + "merchant": items[0].merchant, + "count": len(items), + "items": [ + { + "id": x.id, + "amount": x.amount, + "billing_cycle": x.billing_cycle + } + for x in items + ], + "recommendation": f"You have {len(items)} active subscriptions for {items[0].merchant}. Consolidate to a single account to save." + }) + + # Risk Analysis (utilities vs entertainment) + essential_categories = ["electricity", "water", "gas", "internet", "phone", "insurance"] + for s in subs: + is_essential = any(k in s.merchant.lower() for k in essential_categories) + if is_essential: + risk_analysis.append({ + "merchant": s.merchant, + "risk_level": "high", + "consequences": "Utility interruption, account reactivation fees, or legal service contract breaches." + }) + else: + risk_analysis.append({ + "merchant": s.merchant, + "risk_level": "low", + "consequences": "Loss of entertainment streaming access only. Service can be reactivated instantly." + }) + + return { + "subscriptions": [ + { + "id": s.id, + "merchant": s.merchant, + "amount": s.amount, + "billing_cycle": s.billing_cycle, + "usage_frequency": s.ai_usage_detection.get("usage_frequency", "medium") if s.ai_usage_detection else "medium" + } + for s in subs + ], + "duplicates": duplicates, + "unused_subscriptions": cancellation_suggestions, + "yearly_savings_potential": round(yearly_savings, 2), + "risk_analysis": risk_analysis + } diff --git a/backend/app/ai/voice_assistant.py b/backend/app/ai/voice_assistant.py new file mode 100644 index 0000000000000000000000000000000000000000..71f12b8f3ad8ef85ab72673e5513f452331f813f --- /dev/null +++ b/backend/app/ai/voice_assistant.py @@ -0,0 +1,219 @@ +""" +Voice Banking Assistant for BankBot +Enables voice-based banking queries and responses +""" + +import os +import json +import speech_recognition as sr +import pyttsx3 +from gtts import gTTS +import io +import streamlit as st +from datetime import datetime + +class VoiceAssistant: + """Handles voice input and output for banking queries""" + + def __init__(self): + self.recognizer = sr.Recognizer() + self.engine = pyttsx3.init() + self.engine.setProperty('rate', 150) # Speaking rate + self.microphone = sr.Microphone() + + # Initialize text-to-speech properties + self.engine.setProperty('volume', 0.9) # Volume level (0.0 to 1.0) + + def listen_to_user(self, timeout=10): + """ + Capture audio input from microphone and convert to text + Returns: Recognized text or None if recognition fails + """ + try: + with self.microphone as source: + # Adjust for ambient noise + self.recognizer.adjust_for_ambient_noise(source, duration=0.5) + + # Listen for audio + audio = self.recognizer.listen(source, timeout=timeout, phrase_time_limit=15) + + # Try to recognize using Google Speech Recognition + text = self.recognizer.recognize_google(audio) + return text.lower() + + except sr.RequestError as e: + return None # Could not request results; network error + except sr.UnknownValueError: + return None # Unable to recognize speech + except Exception as e: + print(f"Error listening to user: {e}") + return None + + def speak_response(self, text, use_gtts=False): + """ + Provide audio output for the response + Args: + text: Response text + use_gtts: Use Google Text-to-Speech instead of pyttsx3 + Returns: Audio data or None + """ + try: + if use_gtts: + # Use Google TTS (requires internet, better quality) + tts = gTTS(text=text, lang='en', slow=False) + audio_fp = io.BytesIO() + tts.write_to_fp(audio_fp) + audio_fp.seek(0) + return audio_fp + else: + # Use pyttsx3 (offline, works locally) + self.engine.say(text) + self.engine.runAndWait() + return True + + except Exception as e: + print(f"Error in text-to-speech: {e}") + return None + + def process_voice_query(self, transcribed_text, user_data, transactions): + """ + Process voice query and extract banking intent + Returns: Query type, extracted information + """ + text_lower = transcribed_text.lower() + + # Balance query + if any(word in text_lower for word in ["balance", "how much", "account balance"]): + return "balance", None + + # Transaction history + elif any(word in text_lower for word in ["transactions", "history", "recent", "last"]): + return "transactions", None + + # Spending analysis + elif any(word in text_lower for word in ["spending", "spent", "expenses", "how much did i spend"]): + return "spending", None + + # Transfer query + elif any(word in text_lower for word in ["transfer", "send", "pay"]): + return "transfer", None + + # Loan info + elif any(word in text_lower for word in ["loan", "emi", "borrow", "credit"]): + return "loan", None + + # FD/Investment + elif any(word in text_lower for word in ["fixed deposit", "fd", "invest", "investment"]): + return "fd", None + + # Help/Support + elif any(word in text_lower for word in ["help", "support", "assist", "how do i"]): + return "help", None + + else: + return "general", None + + def generate_voice_response(self, query_type, user_data, transactions, get_ai_response_fn=None): + """ + Generate appropriate response for voice query + Returns: Response text and audio + """ + balance = user_data.get('balance', 0) + + if query_type == "balance": + response = f"Your current account balance is rupees {balance:.2f}" + + elif query_type == "transactions": + recent = transactions[:5] if transactions else [] + if not recent: + response = "You have no recent transactions." + else: + response = f"Your last transaction was {recent[0].get('amount')} rupees for {recent[0].get('details', 'banking service')}" + + elif query_type == "spending": + # Calculate spending + debit_txns = [t for t in transactions if t.get('type') == 'debit'] + total_spent = sum(float(t.get('amount', 0)) for t in debit_txns[-10:]) + response = f"You have spent {total_spent:.2f} rupees in your recent transactions." + + elif query_type == "help": + response = "I can help you with balance inquiries, transaction history, spending analysis, fund transfers, and loan information. What would you like to know?" + + elif query_type == "general" and get_ai_response_fn: + # Use AI for general banking queries + response = get_ai_response_fn("user query", []) + + else: + response = "I didn't quite understand. Could you please rephrase your question?" + + return response + + def extract_voice_command(self, transcribed_text): + """Extract command-specific parameters from voice input""" + # Extract amounts from voice + amount_words = { + "hundred": 100, "thousand": 1000, "lakh": 100000, + "rupees": 1, "rupee": 1, "paisa": 0.01 + } + + # Extract recipient name if present + # Extract date references if present + + return None + +def record_voice_query(username, users_data, get_ai_response_fn): + """ + Record and process voice query through Streamlit UI + """ + st.markdown(""" +
+

🎤 Voice Banking Assistant

+
+ """, unsafe_allow_html=True) + + col1, col2, col3 = st.columns([1, 2, 1]) + + with col2: + if st.button("🎙️ Start Recording", key="voice_record", use_container_width=True): + with st.spinner("🎧 Listening... Speak now!"): + assistant = VoiceAssistant() + recognized_text = assistant.listen_to_user(timeout=10) + + if recognized_text: + st.success(f"✅ Recognized: {recognized_text}") + + # Process the query + user_data = users_data.get(username, {}) + transactions = user_data.get('transactions', []) + + query_type, _ = assistant.process_voice_query(recognized_text, user_data, transactions) + response = assistant.generate_voice_response( + query_type, + user_data, + transactions, + get_ai_response_fn + ) + + # Display response + st.info(f"🤖 Response: {response}") + + # Provide audio feedback + with st.spinner("🔊 Converting to speech..."): + assistant.speak_response(response, use_gtts=False) + + st.success("✅ Response delivered") + else: + st.error("❌ Could not recognize speech. Please try again.") + +def voice_mode_demo(): + """Demo voice banking queries""" + demo_queries = [ + "What's my balance?", + "Show my recent transactions", + "How much did I spend this month?", + "Transfer 5000 to John", + "Tell me about loan eligibility" + ] + + return demo_queries diff --git a/backend/app/auth/__init__.py b/backend/app/auth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py new file mode 100644 index 0000000000000000000000000000000000000000..7ad38015f70b0fad08e2c40cd3f6b4b8ca51b8e5 --- /dev/null +++ b/backend/app/auth/router.py @@ -0,0 +1,189 @@ +""" +Authentication router — JWT login, register, refresh, logout. +Uses bcrypt directly (avoids passlib 1.7.4 + bcrypt>=4 incompatibility). +Uses python-jose for JWT. +""" +from datetime import datetime, timedelta +from typing import Optional + +import bcrypt as _bcrypt +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from pydantic import BaseModel, EmailStr +from sqlalchemy.orm import Session + +from app.database.database import get_db +from app.database.models import User, generate_uuid +import os + +router = APIRouter(prefix="/api/auth", tags=["Authentication"]) + +# ─── Config ─────────────────────────────────────────────────────────────────── +SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "bankbot-dev-secret-change-in-production") +ALGORITHM = os.environ.get("JWT_ALGORITHM", "HS256") +ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", "60")) +REFRESH_TOKEN_EXPIRE_DAYS = 7 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) + +# ─── Schemas ────────────────────────────────────────────────────────────────── +class RegisterRequest(BaseModel): + email: EmailStr + password: str + name: str + +class LoginResponse(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" + user_id: str + name: str + email: str + +class RefreshRequest(BaseModel): + refresh_token: str + +class TokenData(BaseModel): + user_id: Optional[str] = None + token_type: Optional[str] = None + +# ─── Password helpers (bcrypt direct — no passlib) ──────────────────────────── +def hash_password(password: str) -> str: + return _bcrypt.hashpw(password.encode("utf-8"), _bcrypt.gensalt(rounds=12)).decode("utf-8") + +def verify_password(plain: str, hashed: str) -> bool: + try: + return _bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8")) + except Exception: + return False + +# ─── JWT helpers ────────────────────────────────────────────────────────────── +def create_token(data: dict, expires_delta: timedelta) -> str: + payload = data.copy() + payload["exp"] = datetime.utcnow() + expires_delta + return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) + +def create_access_token(user_id: str) -> str: + return create_token( + {"sub": user_id, "type": "access"}, + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), + ) + +def create_refresh_token(user_id: str) -> str: + return create_token( + {"sub": user_id, "type": "refresh"}, + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS), + ) + +def decode_token(token: str) -> TokenData: + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + token_type: str = payload.get("type") + if not user_id: + raise HTTPException(status_code=401, detail="Invalid token payload") + return TokenData(user_id=user_id, token_type=token_type) + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token expired or invalid", + headers={"WWW-Authenticate": "Bearer"}, + ) + +# ─── Auth dependencies ──────────────────────────────────────────────────────── +def get_current_user( + token: Optional[str] = Depends(oauth2_scheme), + db: Session = Depends(get_db), +) -> User: + if not token: + raise HTTPException(status_code=401, detail="Not authenticated") + token_data = decode_token(token) + if token_data.token_type != "access": + raise HTTPException(status_code=401, detail="Invalid token type") + user = db.query(User).filter(User.id == token_data.user_id).first() + if not user: + raise HTTPException(status_code=401, detail="User not found") + return user + +def get_current_user_optional( + token: Optional[str] = Depends(oauth2_scheme), + db: Session = Depends(get_db), +) -> Optional[User]: + if not token: + return None + try: + return get_current_user(token, db) + except HTTPException: + return None + +# ─── Routes ─────────────────────────────────────────────────────────────────── +@router.post("/register", response_model=LoginResponse, status_code=201) +def register(req: RegisterRequest, db: Session = Depends(get_db)): + existing = db.query(User).filter(User.email == req.email).first() + if existing: + raise HTTPException(status_code=409, detail="Email already registered") + + user = User( + id=generate_uuid(), + email=req.email, + password_hash=hash_password(req.password), + profile_data={"name": req.name}, + financial_personality="Balanced", + ) + db.add(user) + db.commit() + db.refresh(user) + + return LoginResponse( + access_token=create_access_token(user.id), + refresh_token=create_refresh_token(user.id), + user_id=user.id, + name=req.name, + email=user.email, + ) + +@router.post("/login", response_model=LoginResponse) +def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = db.query(User).filter(User.email == form.username).first() + if not user or not verify_password(form.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + return LoginResponse( + access_token=create_access_token(user.id), + refresh_token=create_refresh_token(user.id), + user_id=user.id, + name=user.profile_data.get("name", "User"), + email=user.email, + ) + +@router.post("/refresh") +def refresh_token(req: RefreshRequest, db: Session = Depends(get_db)): + token_data = decode_token(req.refresh_token) + if token_data.token_type != "refresh": + raise HTTPException(status_code=401, detail="Invalid refresh token") + user = db.query(User).filter(User.id == token_data.user_id).first() + if not user: + raise HTTPException(status_code=401, detail="User not found") + return { + "access_token": create_access_token(user.id), + "token_type": "bearer", + } + +@router.get("/me") +def get_me(current_user: User = Depends(get_current_user)): + return { + "user_id": current_user.id, + "email": current_user.email, + "name": current_user.profile_data.get("name", "User"), + "financial_personality": current_user.financial_personality, + } + +@router.post("/logout") +def logout(): + # Stateless JWT — client drops the token. + # Production: add token to a Redis blacklist here. + return {"message": "Logged out successfully"} diff --git a/backend/app/dashboard/__init__.py b/backend/app/dashboard/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/dashboard/router.py b/backend/app/dashboard/router.py new file mode 100644 index 0000000000000000000000000000000000000000..a4c92ecb1f208c38e764d068e5b9192471251a49 --- /dev/null +++ b/backend/app/dashboard/router.py @@ -0,0 +1,189 @@ +""" +Dashboard router — aggregated data for the main dashboard page. +Returns balances, recent transactions, spending breakdown, and AI briefing. +""" +from typing import Optional +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from sqlalchemy import func, desc +from datetime import datetime, timedelta + +from app.database.database import get_db +from app.database.models import User, Account, Transaction, AnalyticsSnapshot +from app.middleware.cache import cache +from app.ai.fraud import get_user_fraud_alerts +from collections import defaultdict + +router = APIRouter(prefix="/api/dashboard", tags=["Dashboard"]) + +def _resolve_user(db: Session, user_id: Optional[str]) -> str: + if user_id: + return user_id + user = db.query(User).first() + if not user: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="No users found. Seed the database first.") + return user.id + +@router.get("/overview") +def get_dashboard_overview(user_id: Optional[str] = None, db: Session = Depends(get_db)): + """ + Returns all data needed for the main dashboard in a single request: + - account balances + - monthly income/expense totals + - recent transactions (last 10) + - spending by category (current month) + - financial health score + - AI daily briefing (cached 1h) + - fraud alert count + """ + uid = _resolve_user(db, user_id) + cache_key = f"dashboard:overview:{uid}" + cached = cache.get(cache_key) + if cached: + return cached + + # ── Accounts & balances ────────────────────────────────────────────────── + accounts = db.query(Account).filter(Account.user_id == uid).all() + total_balance = sum(a.balance for a in accounts) + account_list = [ + {"id": a.id, "type": a.type, "balance": a.balance, "currency": a.currency} + for a in accounts + ] + + # ── Current month date range ───────────────────────────────────────────── + now = datetime.utcnow() + month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + # ── Transactions this month (lightweight) ─────────────────────────────── + account_ids = [a.id for a in accounts] + monthly_raw = ( + db.query(Transaction.type, Transaction.amount, Transaction.category) + .filter( + Transaction.account_id.in_(account_ids), + Transaction.timestamp >= month_start, + ) + .all() + ) + + monthly_income = sum(amt for t_type, amt, _ in monthly_raw if t_type == "credit") + monthly_expenses = sum(abs(amt) for t_type, amt, _ in monthly_raw if t_type == "debit") + savings_rate = round((monthly_income - monthly_expenses) / monthly_income * 100, 1) if monthly_income > 0 else 0.0 + + # ── Spending by category ───────────────────────────────────────────────── + category_totals: dict = {} + for t_type, amt, cat in monthly_raw: + if t_type == "debit" and cat: + category_totals[cat] = category_totals.get(cat, 0) + abs(amt) + + spending_by_category = [ + {"name": cat, "value": round(total, 2)} + for cat, total in sorted(category_totals.items(), key=lambda x: -x[1]) + ] + + # ── Recent transactions (last 10) ──────────────────────────────────────── + recent_txns = ( + db.query(Transaction) + .filter(Transaction.account_id.in_(account_ids)) + .order_by(desc(Transaction.timestamp)) + .limit(10) + .all() + ) + recent_list = [ + { + "id": t.id, + "merchant": t.merchant or "Unknown", + "category": t.category or "Other", + "amount": t.amount if t.type == "credit" else -abs(t.amount), + "type": t.type, + "timestamp": t.timestamp.isoformat() if t.timestamp else None, + } + for t in recent_txns + ] + + # ── 6-month cash flow trend (lightweight column-only query) ───────────── + six_months_ago = now - timedelta(days=180) + raw_6m = ( + db.query( + Transaction.type, + Transaction.amount, + Transaction.timestamp, + ) + .filter( + Transaction.account_id.in_(account_ids), + Transaction.timestamp >= six_months_ago, + ) + .all() + ) + + # Group by month label in Python + month_buckets: dict = defaultdict(lambda: {"income": 0.0, "expenses": 0.0}) + for t_type, t_amount, t_ts in raw_6m: + if t_ts: + label = t_ts.strftime("%b") + if t_type == "credit": + month_buckets[label]["income"] += t_amount + else: + month_buckets[label]["expenses"] += abs(t_amount) + + # Build ordered list for last 6 months + cash_flow = [] + for i in range(5, -1, -1): + m_date = (now.replace(day=1) - timedelta(days=i * 30)) + label = m_date.strftime("%b") + inc = round(month_buckets[label]["income"], 2) + exp = round(month_buckets[label]["expenses"], 2) + cash_flow.append({ + "month": label, + "income": inc, + "expenses": exp, + "savings": round(max(inc - exp, 0), 2), + }) + + # ── Financial health score (from cache only — never block on AI) ──────────── + score_data = {} + health_score = 0.0 + try: + score_cache_key = f"ai:coaching:score:{uid}" + score_data = cache.get(score_cache_key) or {} + health_score = score_data.get("overall_score", 0.0) + except Exception: + pass + + # ── Fraud alerts (cached separately) ──────────────────────────────────── + fraud_count = 0 + try: + fraud_cache_key = f"dashboard:fraud:{uid}" + cached_fraud = cache.get(fraud_cache_key) + if cached_fraud is not None: + fraud_count = cached_fraud + else: + fraud_data = get_user_fraud_alerts(db, uid) + fraud_count = len(fraud_data.get("alerts", [])) + cache.set(fraud_cache_key, fraud_count, ttl=300) # 5-min cache + except Exception: + pass + + # ── AI briefing (from cache only — never block on AI) ──────────────────── + briefing_key = f"ai:coaching:briefing:{uid}" + briefing = cache.get(briefing_key) or { + "summary": "Run /api/ai/coaching/briefing to generate your AI daily briefing.", + "briefing": None, + } + + result = { + "total_balance": round(total_balance, 2), + "accounts": account_list, + "monthly_income": round(monthly_income, 2), + "monthly_expenses": round(monthly_expenses, 2), + "savings_rate": savings_rate, + "spending_by_category": spending_by_category, + "recent_transactions": recent_list, + "cash_flow": cash_flow, + "health_score": round(health_score, 1), + "fraud_alert_count": fraud_count, + "ai_briefing": briefing, + } + + cache.set(cache_key, result, ttl=120) # 2-minute cache + return result diff --git a/backend/app/database/database.py b/backend/app/database/database.py new file mode 100644 index 0000000000000000000000000000000000000000..f64b95595838f92fa81d4e62c5a48daf8e1a4726 --- /dev/null +++ b/backend/app/database/database.py @@ -0,0 +1,42 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import OperationalError +import os + +# Read database URL from environment or fallback to docker-compose default +SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://admin:adminpassword@localhost:5432/bankbot") +USE_SQLITE = os.getenv("USE_SQLITE", "false").lower() in ("true", "1", "yes") + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sqlite_db_path = os.path.join(BASE_DIR, "bankbot.db") + +if USE_SQLITE: + SQLALCHEMY_DATABASE_URL = f"sqlite:///{sqlite_db_path}" + +connect_args = {} +if "sqlite" in SQLALCHEMY_DATABASE_URL: + connect_args = {"check_same_thread": False} + +try: + engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args=connect_args) + # Test connection + with engine.connect() as conn: + pass +except (OperationalError, Exception) as e: + print(f"Database connection to {SQLALCHEMY_DATABASE_URL} failed: {e}") + print(f"Falling back to SQLite database at sqlite:///{sqlite_db_path}...") + SQLALCHEMY_DATABASE_URL = f"sqlite:///{sqlite_db_path}" + connect_args = {"check_same_thread": False} + engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args=connect_args) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/database/models.py b/backend/app/database/models.py new file mode 100644 index 0000000000000000000000000000000000000000..23f1ed9d5f34738957ad4df06e89cfc7866c9e5b --- /dev/null +++ b/backend/app/database/models.py @@ -0,0 +1,147 @@ +from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey, Text, JSON +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.database.database import Base +import uuid + +def generate_uuid(): + return str(uuid.uuid4()) + +class User(Base): + __tablename__ = "users" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + email = Column(String, unique=True, index=True, nullable=False) + password_hash = Column(String, nullable=False) + profile_data = Column(JSON, default={}) + financial_personality = Column(String, default="Unknown") + ai_personalization_settings = Column(JSON, default={}) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + accounts = relationship("Account", back_populates="user") + subscriptions = relationship("Subscription", back_populates="user") + goals = relationship("Goal", back_populates="user") + investments = relationship("Investment", back_populates="user") + ai_insights = relationship("AIInsight", back_populates="user") + notifications = relationship("Notification", back_populates="user") + analytics_snapshots = relationship("AnalyticsSnapshot", back_populates="user") + +class Account(Base): + __tablename__ = "accounts" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + type = Column(String, nullable=False) # e.g. checking, savings + balance = Column(Float, default=0.0) + currency = Column(String, default="USD") + status = Column(String, default="active") + + user = relationship("User", back_populates="accounts") + transactions = relationship("Transaction", back_populates="account") + +class Transaction(Base): + __tablename__ = "transactions" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + account_id = Column(String, ForeignKey("accounts.id"), nullable=False) + amount = Column(Float, nullable=False) + type = Column(String, nullable=False) # credit, debit + category = Column(String) + timestamp = Column(DateTime(timezone=True), server_default=func.now()) + merchant = Column(String) + tags = Column(JSON, default=[]) + ai_generated_metadata = Column(JSON, default={}) + spending_emotion_label = Column(String) + + account = relationship("Account", back_populates="transactions") + fraud_log = relationship("FraudLog", back_populates="transaction", uselist=False) + +class Subscription(Base): + __tablename__ = "subscriptions" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + merchant = Column(String, nullable=False) + amount = Column(Float, nullable=False) + billing_cycle = Column(String, nullable=False) # monthly, yearly + active = Column(Boolean, default=True) + ai_usage_detection = Column(JSON, default={}) + + user = relationship("User", back_populates="subscriptions") + +class Goal(Base): + __tablename__ = "goals" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + title = Column(String, nullable=False) + target_amount = Column(Float, nullable=False) + current_amount = Column(Float, default=0.0) + target_date = Column(DateTime(timezone=True)) + ai_generated_plan = Column(JSON, default={}) + + user = relationship("User", back_populates="goals") + +class Investment(Base): + __tablename__ = "investments" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + asset_name = Column(String, nullable=False) + type = Column(String, nullable=False) # stock, crypto, mutual_fund + amount_invested = Column(Float, default=0.0) + current_value = Column(Float, default=0.0) + portfolio_allocation = Column(Float, default=0.0) + ai_risk_analysis = Column(JSON, default={}) + + user = relationship("User", back_populates="investments") + +class AIInsight(Base): + __tablename__ = "ai_insights" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + type = Column(String, nullable=False) # recommendation, briefing, cashflow + content = Column(Text, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="ai_insights") + +class FraudLog(Base): + __tablename__ = "fraud_logs" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + transaction_id = Column(String, ForeignKey("transactions.id"), nullable=False) + risk_score = Column(Float, nullable=False) + suspicious_activity_details = Column(Text) + status = Column(String, default="pending") # pending, resolved, false_positive + + transaction = relationship("Transaction", back_populates="fraud_log") + +class Notification(Base): + __tablename__ = "notifications" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + title = Column(String, nullable=False) + message = Column(Text, nullable=False) + type = Column(String, nullable=False) # alert, insight, warning + read_status = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="notifications") + +class AnalyticsSnapshot(Base): + __tablename__ = "analytics_snapshots" + + id = Column(String, primary_key=True, index=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + date = Column(DateTime(timezone=True), nullable=False) + total_balance = Column(Float, default=0.0) + total_spending = Column(Float, default=0.0) + total_savings = Column(Float, default=0.0) + financial_score = Column(Float, default=0.0) + trends_json = Column(JSON, default={}) + + user = relationship("User", back_populates="analytics_snapshots") diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..90e7c9af9b7250951a6b81db67925797986a8355 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,171 @@ +""" +BankBot FastAPI — production entry point. +Phase 7: structured logging, metrics, security headers, rate limiting. +""" +import json +import os +import time +from collections import defaultdict + +from fastapi import FastAPI, Request, Response +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse + +from app.database.database import engine, Base +import app.database.models # noqa: F401 + +# ─── Routers ────────────────────────────────────────────────────────────────── +from app.ai.router import router as ai_router +from app.websocket.router import router as ws_router +from app.auth.router import router as auth_router +from app.dashboard.router import router as dashboard_router +from app.notifications.router import router as notifications_router +from app.transactions.router import router as transactions_router + +# ─── Observability ──────────────────────────────────────────────────────────── +from app.middleware.logging import RequestLoggingMiddleware, metrics, api_logger + +# ─── App ────────────────────────────────────────────────────────────────────── +app = FastAPI( + title="BankBot AI API", + description="Production-grade AI-powered financial platform", + version="2.0.0", + docs_url="/docs", + redoc_url="/redoc", +) + +# ─── CORS ───────────────────────────────────────────────────────────────────── +_raw = os.environ.get("BACKEND_CORS_ORIGINS", '["http://localhost:3000","http://localhost:7860"]') +try: + allowed_origins = json.loads(_raw) +except Exception: + allowed_origins = ["http://localhost:3000", "http://localhost:7860"] + +# In HF Spaces, the Space URL is dynamic — allow all *.hf.space origins +# by using allow_origin_regex as a fallback +HF_SPACE_PATTERN = r"https://.*\.hf\.space" + +app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_origin_regex=HF_SPACE_PATTERN, + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allow_headers=["Authorization", "Content-Type", "X-Request-ID"], + expose_headers=["X-Request-ID", "X-Process-Time"], +) + +# ─── Request logging ────────────────────────────────────────────────────────── +app.add_middleware(RequestLoggingMiddleware) + +# ─── Security headers ───────────────────────────────────────────────────────── +@app.middleware("http") +async def security_headers(request: Request, call_next): + response: Response = await call_next(request) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" + return response + +# ─── Process-time header ────────────────────────────────────────────────────── +@app.middleware("http") +async def process_time_header(request: Request, call_next): + t = time.time() + response = await call_next(request) + response.headers["X-Process-Time"] = f"{(time.time()-t)*1000:.1f}ms" + return response + +# ─── Rate limiter ───────────────────────────────────────────────────────────── +_rate_store: dict = defaultdict(list) +RATE_LIMIT = 120 +RATE_WINDOW = 60 + +@app.middleware("http") +async def rate_limit(request: Request, call_next): + skip = request.url.path in ("/health", "/") or \ + "websocket" in request.headers.get("upgrade", "").lower() + if skip: + return await call_next(request) + + ip = request.client.host if request.client else "unknown" + now = time.time() + _rate_store[ip] = [t for t in _rate_store[ip] if t > now - RATE_WINDOW] + + if len(_rate_store[ip]) >= RATE_LIMIT: + metrics.record_error(request.url.path, 429, "rate_limited") + return JSONResponse( + status_code=429, + content={"detail": "Too many requests. Please slow down."}, + headers={"Retry-After": str(RATE_WINDOW)}, + ) + _rate_store[ip].append(now) + return await call_next(request) + +# ─── Startup ────────────────────────────────────────────────────────────────── +@app.on_event("startup") +def startup(): + api_logger.info("BankBot API starting", extra={"version": "2.0.0"}) + Base.metadata.create_all(bind=engine) + api_logger.info("Database tables ready") + + # Log active backends + from app.ai.ollama_integration import OPENAI_API_KEY, GROQ_API_KEY, has_active_ai_backend + from app.middleware.cache import cache + from app.database.database import SQLALCHEMY_DATABASE_URL + + ai_backend = "openai" if OPENAI_API_KEY else ("groq" if GROQ_API_KEY else "ollama") + api_logger.info("Startup diagnostics", extra={ + "ai_backend": ai_backend, + "ai_available": has_active_ai_backend(), + "db_type": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", + "cache_type": "redis" if cache.use_redis else "memory", + }) + +# ─── Routers ────────────────────────────────────────────────────────────────── +app.include_router(auth_router) +app.include_router(ai_router) +app.include_router(ws_router) +app.include_router(dashboard_router) +app.include_router(notifications_router) +app.include_router(transactions_router) + +# ─── Core endpoints ─────────────────────────────────────────────────────────── +@app.get("/", tags=["Core"]) +def root(): + return {"message": "BankBot AI API v2.0", "status": "operational", "docs": "/docs"} + +@app.get("/health", tags=["Core"]) +def health(): + from app.middleware.cache import cache + from app.database.database import SQLALCHEMY_DATABASE_URL + return { + "status": "healthy", + "timestamp": time.time(), + "db": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", + "cache": "redis" if cache.use_redis else "memory", + "uptime_s": round(time.time() - metrics.start_time, 0), + } + +@app.get("/api/status", tags=["Core"]) +def api_status(): + from app.ai.ollama_integration import has_active_ai_backend, OPENAI_API_KEY, GROQ_API_KEY + from app.middleware.cache import cache + from app.database.database import SQLALCHEMY_DATABASE_URL + ai = "openai" if OPENAI_API_KEY else ("groq" if GROQ_API_KEY else "ollama") + return { + "ai_backend": ai, + "ai_available": has_active_ai_backend(), + "db_type": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", + "cache_type": "redis" if cache.use_redis else "memory", + "version": "2.0.0", + } + +@app.get("/api/metrics", tags=["Observability"]) +def get_metrics(): + """ + Live observability dashboard — request counts, AI latency, + cache hit ratio, WebSocket stats, recent errors. + """ + return metrics.summary() diff --git a/backend/app/middleware/__init__.py b/backend/app/middleware/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/middleware/cache.py b/backend/app/middleware/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..972e876ac86414bf6fbddb6ec4a45a6210b65905 --- /dev/null +++ b/backend/app/middleware/cache.py @@ -0,0 +1,86 @@ +import os +import json +import time +from threading import Lock + +try: + import redis +except ImportError: + redis = None + +class MemoryCache: + def __init__(self): + self._cache = {} + self._lock = Lock() + + def get(self, key): + with self._lock: + if key not in self._cache: + return None + val, expiry = self._cache[key] + if expiry is not None and time.time() > expiry: + del self._cache[key] + return None + return val + + def set(self, key, value, ttl=None): + with self._lock: + expiry = time.time() + ttl if ttl is not None else None + self._cache[key] = (value, expiry) + + def delete(self, key): + with self._lock: + if key in self._cache: + del self._cache[key] + +class CacheManager: + def __init__(self): + self.redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0") + self.redis_client = None + self.use_redis = False + + if redis is not None: + try: + self.redis_client = redis.Redis.from_url(self.redis_url, socket_timeout=1.0) + # Test connection + self.redis_client.ping() + self.use_redis = True + print("Connected to Redis successfully.") + except Exception as e: + print(f"Redis connection failed ({e}). Falling back to in-memory cache.") + else: + print("Redis library not installed. Falling back to in-memory cache.") + + self.memory_cache = MemoryCache() + + def get(self, key: str): + if self.use_redis: + try: + val = self.redis_client.get(key) + if val: + return json.loads(val.decode('utf-8')) + except Exception as e: + # Fallback to memory on Redis error during operation + print(f"Redis get failed ({e}). Using memory cache fallback.") + return self.memory_cache.get(key) + + def set(self, key: str, value, ttl: int = None): + if self.use_redis: + try: + self.redis_client.set(key, json.dumps(value), ex=ttl) + return + except Exception as e: + print(f"Redis set failed ({e}). Using memory cache fallback.") + self.memory_cache.set(key, value, ttl) + + def delete(self, key: str): + if self.use_redis: + try: + self.redis_client.delete(key) + return + except Exception as e: + print(f"Redis delete failed ({e}). Using memory cache fallback.") + self.memory_cache.delete(key) + +# Global cache instance +cache = CacheManager() diff --git a/backend/app/middleware/logging.py b/backend/app/middleware/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..e6e30c6f07b3eaeb6923b8f4b57b1b3c87ca14be --- /dev/null +++ b/backend/app/middleware/logging.py @@ -0,0 +1,184 @@ +""" +Structured logging middleware — JSON logs with request tracing, +timing, AI provider health, cache hit ratios, and WebSocket events. +""" +import json +import logging +import time +import uuid +from collections import defaultdict, deque +from datetime import datetime +from typing import Callable + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware + +# ─── Structured JSON logger ─────────────────────────────────────────────────── +class JSONFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + log = { + "ts": datetime.utcnow().isoformat() + "Z", + "level": record.levelname, + "logger": record.name, + "msg": record.getMessage(), + } + if hasattr(record, "extra"): + log.update(record.extra) + if record.exc_info: + log["exc"] = self.formatException(record.exc_info) + return json.dumps(log) + +def get_logger(name: str) -> logging.Logger: + logger = logging.getLogger(name) + if not logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(JSONFormatter()) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + logger.propagate = False + return logger + +api_logger = get_logger("bankbot.api") +ai_logger = get_logger("bankbot.ai") +ws_logger = get_logger("bankbot.ws") +db_logger = get_logger("bankbot.db") + +# ─── In-process metrics store ───────────────────────────────────────────────── +class MetricsStore: + """Thread-safe in-memory metrics — no external dependency.""" + + def __init__(self): + self.request_count: int = 0 + self.error_count: int = 0 + self.auth_failures: int = 0 + self.ws_connects: int = 0 + self.ws_reconnects: int = 0 + self.ai_calls: dict = defaultdict(int) # provider → count + self.ai_errors: dict = defaultdict(int) # provider → errors + self.ai_latencies: dict = defaultdict(list) # provider → [ms] + self.ai_fallbacks: int = 0 + self.cache_hits: int = 0 + self.cache_misses: int = 0 + self.route_timings: dict = defaultdict(list) # path → [ms] + self._recent_errors: deque = deque(maxlen=50) # last 50 errors + self.start_time: float = time.time() + + # ── AI tracking ────────────────────────────────────────────────────────── + def record_ai_call(self, provider: str, latency_ms: float, success: bool): + self.ai_calls[provider] += 1 + self.ai_latencies[provider].append(latency_ms) + if len(self.ai_latencies[provider]) > 200: + self.ai_latencies[provider] = self.ai_latencies[provider][-200:] + if not success: + self.ai_errors[provider] += 1 + + def record_ai_fallback(self): + self.ai_fallbacks += 1 + + # ── Cache tracking ──────────────────────────────────────────────────────── + def record_cache_hit(self): + self.cache_hits += 1 + + def record_cache_miss(self): + self.cache_misses += 1 + + # ── Error tracking ──────────────────────────────────────────────────────── + def record_error(self, path: str, status: int, detail: str): + self._recent_errors.append({ + "ts": datetime.utcnow().isoformat() + "Z", + "path": path, + "status": status, + "detail": detail[:200], + }) + self.error_count += 1 + if status == 401: + self.auth_failures += 1 + + # ── Summary ─────────────────────────────────────────────────────────────── + def summary(self) -> dict: + uptime = time.time() - self.start_time + cache_total = self.cache_hits + self.cache_misses + cache_ratio = round(self.cache_hits / cache_total * 100, 1) if cache_total else 0 + + ai_summary = {} + for provider in set(list(self.ai_calls.keys()) + list(self.ai_errors.keys())): + lats = self.ai_latencies.get(provider, []) + ai_summary[provider] = { + "calls": self.ai_calls[provider], + "errors": self.ai_errors[provider], + "avg_latency_ms": round(sum(lats) / len(lats), 1) if lats else 0, + "p95_latency_ms": round(sorted(lats)[int(len(lats) * 0.95)], 1) if len(lats) >= 20 else None, + } + + slow_routes = {} + for path, times in self.route_timings.items(): + if times: + slow_routes[path] = { + "calls": len(times), + "avg_ms": round(sum(times) / len(times), 1), + "max_ms": round(max(times), 1), + } + + return { + "uptime_seconds": round(uptime, 0), + "requests": { + "total": self.request_count, + "errors": self.error_count, + "auth_failures": self.auth_failures, + "error_rate_pct": round(self.error_count / max(self.request_count, 1) * 100, 2), + }, + "websocket": { + "total_connects": self.ws_connects, + "reconnects": self.ws_reconnects, + }, + "ai": { + "fallbacks": self.ai_fallbacks, + "by_provider": ai_summary, + }, + "cache": { + "hits": self.cache_hits, + "misses": self.cache_misses, + "hit_ratio_pct": cache_ratio, + }, + "route_timings": dict(sorted(slow_routes.items(), key=lambda x: -x[1]["avg_ms"])[:10]), + "recent_errors": list(self._recent_errors)[-10:], + } + +metrics = MetricsStore() + +# ─── Request logging middleware ─────────────────────────────────────────────── +class RequestLoggingMiddleware(BaseHTTPMiddleware): + SKIP_PATHS = {"/health", "/openapi.json", "/docs", "/redoc", "/docs/oauth2-redirect"} + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + if request.url.path in self.SKIP_PATHS: + return await call_next(request) + + request_id = str(uuid.uuid4())[:8] + start = time.time() + metrics.request_count += 1 + + response = await call_next(request) + + elapsed_ms = (time.time() - start) * 1000 + path = request.url.path + metrics.route_timings[path].append(elapsed_ms) + if len(metrics.route_timings[path]) > 500: + metrics.route_timings[path] = metrics.route_timings[path][-500:] + + level = logging.WARNING if elapsed_ms > 2000 else logging.INFO + if response.status_code >= 400: + metrics.record_error(path, response.status_code, "") + level = logging.WARNING if response.status_code < 500 else logging.ERROR + + api_logger.log(level, f"{request.method} {path}", extra={ + "request_id": request_id, + "method": request.method, + "path": path, + "status": response.status_code, + "duration_ms": round(elapsed_ms, 1), + "ip": request.client.host if request.client else "unknown", + }) + + response.headers["X-Request-ID"] = request_id + return response diff --git a/backend/app/notifications/__init__.py b/backend/app/notifications/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/notifications/router.py b/backend/app/notifications/router.py new file mode 100644 index 0000000000000000000000000000000000000000..9f60673faf3f7b643bdb2288ed1343f3cc507759 --- /dev/null +++ b/backend/app/notifications/router.py @@ -0,0 +1,90 @@ +""" +Notifications router — CRUD for user notifications with WebSocket push support. +""" +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import desc + +from app.database.database import get_db +from app.database.models import Notification, User, generate_uuid +from datetime import datetime + +router = APIRouter(prefix="/api/notifications", tags=["Notifications"]) + +def _resolve_user(db: Session, user_id: Optional[str]) -> str: + if user_id: + return user_id + user = db.query(User).first() + if not user: + raise HTTPException(status_code=404, detail="No users found.") + return user.id + +@router.get("/") +def get_notifications(user_id: Optional[str] = None, limit: int = 20, db: Session = Depends(get_db)): + uid = _resolve_user(db, user_id) + notifications = ( + db.query(Notification) + .filter(Notification.user_id == uid) + .order_by(desc(Notification.created_at)) + .limit(limit) + .all() + ) + return { + "notifications": [ + { + "id": n.id, + "title": n.title, + "message": n.message, + "type": n.type, + "read": n.read_status, + "created_at": n.created_at.isoformat() if n.created_at else None, + } + for n in notifications + ], + "unread_count": sum(1 for n in notifications if not n.read_status), + } + +@router.patch("/{notification_id}/read") +def mark_notification_read( + notification_id: str, + user_id: Optional[str] = None, + db: Session = Depends(get_db) +): + uid = _resolve_user(db, user_id) + notif = db.query(Notification).filter( + Notification.id == notification_id, + Notification.user_id == uid + ).first() + if not notif: + raise HTTPException(status_code=404, detail="Notification not found") + notif.read_status = True + db.commit() + return {"success": True} + +@router.patch("/read-all") +def mark_all_read(user_id: Optional[str] = None, db: Session = Depends(get_db)): + uid = _resolve_user(db, user_id) + db.query(Notification).filter( + Notification.user_id == uid, + Notification.read_status == False + ).update({"read_status": True}) + db.commit() + return {"success": True} + +@router.delete("/{notification_id}") +def delete_notification( + notification_id: str, + user_id: Optional[str] = None, + db: Session = Depends(get_db) +): + uid = _resolve_user(db, user_id) + notif = db.query(Notification).filter( + Notification.id == notification_id, + Notification.user_id == uid + ).first() + if not notif: + raise HTTPException(status_code=404, detail="Notification not found") + db.delete(notif) + db.commit() + return {"success": True} diff --git a/backend/app/scripts/seed.py b/backend/app/scripts/seed.py new file mode 100644 index 0000000000000000000000000000000000000000..bb25fc8c34f79812bbbd061bd1545d3c40bffab2 --- /dev/null +++ b/backend/app/scripts/seed.py @@ -0,0 +1,194 @@ +import os +import sys +import uuid +import random +from datetime import datetime, timedelta + +# Add parent directory to path so we can import from app +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from app.database.database import SessionLocal, engine, SQLALCHEMY_DATABASE_URL +from app.database.models import ( + Base, User, Account, Transaction, Subscription, + Goal, Investment, AIInsight, FraudLog, Notification, AnalyticsSnapshot +) + +# Create tables +Base.metadata.create_all(bind=engine) + +def seed_data(): + db = SessionLocal() + + print(f"Seeding into: {SQLALCHEMY_DATABASE_URL}") + + # Check if we already have users + if db.query(User).count() > 0: + print("Database already seeded.") + db.close() + return + + print("Seeding database...") + + personas = ["Saver", "Investor", "Impulsive Spender", "Minimalist", "Risk Taker"] + + merchants = ["Swiggy", "Amazon", "Netflix", "Uber", "Fuel", "Salary", "SIP", + "Starbucks", "Apple", "Walmart"] + categories = ["Food", "Shopping", "Entertainment", "Transport", "Income", + "Investment", "Groceries", "Tech", "Utilities"] + + for persona in personas: + try: + user = User( + email=f"{persona.lower().replace(' ', '_')}@example.com", + password_hash="hashed_password", + profile_data={"name": f"{persona} User", "phone": "+1234567890"}, + financial_personality=persona, + ai_personalization_settings={"theme": "dark", "notifications": "all"} + ) + db.add(user) + db.flush() # get user.id without committing + + # ── Accounts ────────────────────────────────────────────────────── + checking = Account( + user_id=user.id, type="checking", + balance=random.uniform(1000.0, 10000.0), currency="USD" + ) + savings = Account( + user_id=user.id, type="savings", + balance=random.uniform(5000.0, 50000.0), currency="USD" + ) + db.add_all([checking, savings]) + db.flush() + + # ── Subscriptions ───────────────────────────────────────────────── + # Active subscription (high usage) + db.add(Subscription( + user_id=user.id, merchant="Netflix", amount=15.99, + billing_cycle="monthly", active=True, + ai_usage_detection={"usage_frequency": "high", "recommendation": "keep"} + )) + # Unused subscription — triggers unused detection in subscriptions.py + db.add(Subscription( + user_id=user.id, merchant="Spotify", amount=9.99, + billing_cycle="monthly", active=True, + ai_usage_detection={"usage_frequency": "low", "recommendation": "cancel"} + )) + # Duplicate subscription — triggers duplicate detection in subscriptions.py + # (second Netflix entry for the same user) + db.add(Subscription( + user_id=user.id, merchant="Netflix", amount=15.99, + billing_cycle="monthly", active=True, + ai_usage_detection={"usage_frequency": "medium", "recommendation": "review"} + )) + + # ── Goals ───────────────────────────────────────────────────────── + db.add(Goal( + user_id=user.id, title="Emergency Fund", + target_amount=10000.0, + current_amount=random.uniform(1000.0, 5000.0), + target_date=datetime.utcnow() + timedelta(days=365), + ai_generated_plan={"monthly_saving_required": 500.0, "risk": "low"} + )) + + # ── Investments ─────────────────────────────────────────────────── + db.add(Investment( + user_id=user.id, asset_name="S&P 500", type="stock", + amount_invested=random.uniform(1000.0, 10000.0), + current_value=random.uniform(1100.0, 12000.0), + portfolio_allocation=50.0, + ai_risk_analysis={"risk_level": "medium", "recommendation": "hold"} + )) + + # ── Transactions ────────────────────────────────────────────────── + start_date = datetime.utcnow() - timedelta(days=90) + + # Monthly salary (3 months) + for i in range(3): + tx_date = start_date + timedelta(days=i * 30) + db.add(Transaction( + account_id=checking.id, amount=5000.0, type="credit", + category="Income", timestamp=tx_date, merchant="Salary", + tags=["salary", "income"], + ai_generated_metadata={"is_recurring": True, "confidence": 0.99}, + spending_emotion_label="neutral" + )) + + # Regular expense transactions + for _ in range(30): + tx_date = start_date + timedelta(days=random.randint(0, 89)) + amount = random.uniform(10.0, 500.0) + merchant = random.choice(merchants) + + if merchant == "Salary": + continue + + # Persona-based spending adjustments + if user.financial_personality == "Saver" and amount > 200: + amount = random.uniform(10.0, 100.0) + elif user.financial_personality == "Impulsive Spender": + amount = random.uniform(50.0, 800.0) + + tx = Transaction( + account_id=checking.id, amount=amount, type="debit", + category=random.choice(categories), + timestamp=tx_date, merchant=merchant, + tags=["expense"], + ai_generated_metadata={"category_confidence": 0.9}, + spending_emotion_label=random.choice(["happy", "regret", "neutral", "essential"]) + ) + db.add(tx) + db.flush() + + # Seed a fraud log for ~5% of transactions + if random.random() < 0.05: + db.add(FraudLog( + transaction_id=tx.id, + risk_score=random.uniform(0.7, 0.99), + suspicious_activity_details="Unusual location and high amount for this merchant.", + status="pending" + )) + + # Late-night transaction — ensures behavior.py late-night detection fires + late_night_date = start_date + timedelta(days=random.randint(1, 80), + hours=23, minutes=random.randint(0, 59)) + db.add(Transaction( + account_id=checking.id, + amount=random.uniform(50.0, 300.0), + type="debit", + category="Entertainment", + timestamp=late_night_date, + merchant="Online Store", + tags=["late-night", "impulse"], + ai_generated_metadata={"category_confidence": 0.85}, + spending_emotion_label="regret" + )) + + # ── Supporting records ──────────────────────────────────────────── + db.add(AIInsight( + user_id=user.id, type="cashflow", + content=f"You are spending 20% more on {random.choice(categories)} this month." + )) + db.add(Notification( + user_id=user.id, title="Weekly Summary", + message="Your weekly financial summary is ready.", type="insight" + )) + db.add(AnalyticsSnapshot( + user_id=user.id, date=datetime.utcnow(), + total_balance=checking.balance + savings.balance, + total_spending=2000.0, total_savings=3000.0, + financial_score=random.uniform(60.0, 95.0), + trends_json={"spending_trend": "down", "savings_trend": "up"} + )) + + db.commit() + print(f" ✓ Seeded user: {persona}") + + except Exception as e: + db.rollback() + print(f" ✗ Failed to seed user '{persona}': {e}") + + db.close() + print("Database seeded successfully!") + +if __name__ == "__main__": + seed_data() diff --git a/backend/app/scripts/seed_demo.py b/backend/app/scripts/seed_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..2d2c40555a245a0b6165a667ff29526948f3036d --- /dev/null +++ b/backend/app/scripts/seed_demo.py @@ -0,0 +1,300 @@ +""" +Demo seed script — creates a polished demo account (alex@bankbot.dev) +with realistic financial data: transactions, goals, investments, +subscriptions, notifications, and a fraud alert. + +Run: python app/scripts/seed_demo.py +""" +import os +import sys +import uuid +import random +from datetime import datetime, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +import bcrypt as _bcrypt +from app.database.database import SessionLocal, engine +from app.database.models import ( + Base, User, Account, Transaction, Subscription, + Goal, Investment, Notification, FraudLog +) + +Base.metadata.create_all(bind=engine) + +DEMO_EMAIL = "alex@bankbot.dev" +DEMO_PASSWORD = "BankBot2026!" +DEMO_NAME = "Alex Doe" + +def hash_pw(pw: str) -> str: + return _bcrypt.hashpw(pw.encode(), _bcrypt.gensalt(rounds=12)).decode() + +def uid() -> str: + return str(uuid.uuid4()) + +def seed(): + db = SessionLocal() + try: + # ── Remove existing demo user ──────────────────────────────────────── + existing = db.query(User).filter(User.email == DEMO_EMAIL).first() + if existing: + db.delete(existing) + db.commit() + print(f"Removed existing demo user: {DEMO_EMAIL}") + + # ── Create demo user ───────────────────────────────────────────────── + user = User( + id=uid(), + email=DEMO_EMAIL, + password_hash=hash_pw(DEMO_PASSWORD), + profile_data={ + "name": DEMO_NAME, + "phone": "+1 (555) 012-3456", + "avatar": "AD", + "member_since": "2023-01-15", + "plan": "Premium", + }, + financial_personality="Balanced Investor", + ai_personalization_settings={ + "risk_tolerance": "moderate", + "investment_horizon": "long_term", + "notifications": "all", + "ai_tone": "analytical", + }, + ) + db.add(user) + db.flush() + print(f"Created demo user: {DEMO_EMAIL}") + + # ── Accounts ───────────────────────────────────────────────────────── + checking = Account(id=uid(), user_id=user.id, type="checking", + balance=12_847.32, currency="USD", status="active") + savings = Account(id=uid(), user_id=user.id, type="savings", + balance=28_450.00, currency="USD", status="active") + invest = Account(id=uid(), user_id=user.id, type="investment", + balance=18_340.50, currency="USD", status="active") + db.add_all([checking, savings, invest]) + db.flush() + print("Created 3 accounts (checking $12,847 | savings $28,450 | investment $18,340)") + + # ── Transactions — 6 months of realistic data ───────────────────────── + now = datetime.utcnow() + merchants = [ + # (name, category, type, amount_range) + ("Salary Deposit", "Income", "credit", (4800, 5200)), + ("Freelance Payment", "Income", "credit", (800, 2000)), + ("Whole Foods", "Groceries", "debit", (45, 180)), + ("Trader Joe's", "Groceries", "debit", (30, 120)), + ("Netflix", "Entertainment", "debit", (15, 16)), + ("Spotify", "Entertainment", "debit", (9, 10)), + ("Amazon", "Shopping", "debit", (25, 250)), + ("Apple Store", "Tech", "debit", (10, 200)), + ("Uber", "Transport", "debit", (8, 45)), + ("Shell Gas", "Transport", "debit", (40, 80)), + ("Starbucks", "Food", "debit", (5, 18)), + ("Chipotle", "Food", "debit", (10, 25)), + ("Planet Fitness", "Health", "debit", (24, 25)), + ("CVS Pharmacy", "Health", "debit", (12, 60)), + ("Con Edison", "Utilities", "debit", (80, 140)), + ("Verizon", "Utilities", "debit", (85, 90)), + ("Rent Payment", "Housing", "debit", (1950, 1950)), + ("Dividend Income", "Investment", "credit", (120, 350)), + ("Restaurant", "Food", "debit", (30, 90)), + ("Target", "Shopping", "debit", (40, 150)), + ] + + txns = [] + for month_offset in range(6): + month_start = (now.replace(day=1) - timedelta(days=month_offset * 30)) + # Salary on 1st + txns.append(Transaction( + id=uid(), account_id=checking.id, + amount=random.uniform(4800, 5200), type="credit", + category="Income", merchant="Salary Deposit", + timestamp=month_start + timedelta(hours=9), + tags=["recurring", "income"], + )) + # Rent on 3rd + txns.append(Transaction( + id=uid(), account_id=checking.id, + amount=1950.00, type="debit", + category="Housing", merchant="Rent Payment", + timestamp=month_start + timedelta(days=2, hours=10), + tags=["recurring", "housing"], + )) + # Random daily transactions + for _ in range(random.randint(18, 28)): + m = random.choice(merchants[2:]) # skip salary/rent + days_offset = random.randint(0, 28) + hours_offset = random.randint(7, 22) + txns.append(Transaction( + id=uid(), account_id=checking.id, + amount=round(random.uniform(*m[3]), 2), + type=m[2], category=m[1], merchant=m[0], + timestamp=month_start + timedelta(days=days_offset, hours=hours_offset), + tags=[m[1].lower()], + )) + + # One suspicious transaction for fraud demo + fraud_txn = Transaction( + id=uid(), account_id=checking.id, + amount=847.00, type="debit", + category="Shopping", merchant="Tech Store NYC", + timestamp=now - timedelta(hours=2), + tags=["flagged"], + spending_emotion_label="impulsive", + ) + txns.append(fraud_txn) + db.add_all(txns) + db.flush() + print(f"Created {len(txns)} transactions across 6 months") + + # ── Fraud log for the suspicious transaction ────────────────────────── + fraud_log = FraudLog( + id=uid(), + transaction_id=fraud_txn.id, + risk_score=0.78, + suspicious_activity_details=( + "Transaction amount ($847.00) is 3.2x above historical average. " + "Location anomaly: merchant in NYC, usual activity in Brooklyn. " + "Placed at 11:47 PM — outside normal spending hours." + ), + status="pending", + ) + db.add(fraud_log) + print("Created fraud alert for demo transaction") + + # ── Goals ───────────────────────────────────────────────────────────── + goals = [ + Goal(id=uid(), user_id=user.id, title="Emergency Fund", + target_amount=18_000, current_amount=14_200, + target_date=now + timedelta(days=90), + ai_generated_plan={"monthly_contribution": 1267, "months_remaining": 3}), + Goal(id=uid(), user_id=user.id, title="Europe Vacation", + target_amount=5_000, current_amount=2_800, + target_date=now + timedelta(days=180), + ai_generated_plan={"monthly_contribution": 367, "months_remaining": 6}), + Goal(id=uid(), user_id=user.id, title="MacBook Pro", + target_amount=2_500, current_amount=1_900, + target_date=now + timedelta(days=45), + ai_generated_plan={"monthly_contribution": 300, "months_remaining": 2}), + Goal(id=uid(), user_id=user.id, title="Down Payment Fund", + target_amount=80_000, current_amount=28_450, + target_date=now + timedelta(days=730), + ai_generated_plan={"monthly_contribution": 2148, "months_remaining": 24}), + ] + db.add_all(goals) + print(f"Created {len(goals)} financial goals") + + # ── Investments ─────────────────────────────────────────────────────── + investments = [ + Investment(id=uid(), user_id=user.id, asset_name="S&P 500 Index Fund", + type="mutual_fund", amount_invested=8_000, current_value=9_840, + portfolio_allocation=53.6, + ai_risk_analysis={"risk": "moderate", "expected_return": "8-10%", "recommendation": "hold"}), + Investment(id=uid(), user_id=user.id, asset_name="Apple Inc (AAPL)", + type="stock", amount_invested=3_000, current_value=3_720, + portfolio_allocation=20.3, + ai_risk_analysis={"risk": "moderate-high", "expected_return": "12-15%", "recommendation": "hold"}), + Investment(id=uid(), user_id=user.id, asset_name="Bitcoin (BTC)", + type="crypto", amount_invested=2_500, current_value=2_980, + portfolio_allocation=16.2, + ai_risk_analysis={"risk": "high", "expected_return": "variable", "recommendation": "reduce_exposure"}), + Investment(id=uid(), user_id=user.id, asset_name="US Treasury Bonds", + type="bond", amount_invested=1_800, current_value=1_800, + portfolio_allocation=9.8, + ai_risk_analysis={"risk": "low", "expected_return": "4.5%", "recommendation": "hold"}), + ] + db.add_all(investments) + print(f"Created {len(investments)} investments (total value: ${sum(i.current_value for i in investments):,.0f})") + + # ── Subscriptions ───────────────────────────────────────────────────── + subscriptions = [ + Subscription(id=uid(), user_id=user.id, merchant="Netflix", + amount=15.99, billing_cycle="monthly", active=True, + ai_usage_detection={"last_used": "2 days ago", "usage_frequency": "high"}), + Subscription(id=uid(), user_id=user.id, merchant="Spotify", + amount=9.99, billing_cycle="monthly", active=True, + ai_usage_detection={"last_used": "today", "usage_frequency": "daily"}), + Subscription(id=uid(), user_id=user.id, merchant="Adobe Creative Cloud", + amount=54.99, billing_cycle="monthly", active=True, + ai_usage_detection={"last_used": "45 days ago", "usage_frequency": "low"}), + Subscription(id=uid(), user_id=user.id, merchant="Planet Fitness", + amount=24.99, billing_cycle="monthly", active=True, + ai_usage_detection={"last_used": "1 week ago", "usage_frequency": "medium"}), + Subscription(id=uid(), user_id=user.id, merchant="iCloud Storage", + amount=2.99, billing_cycle="monthly", active=True, + ai_usage_detection={"last_used": "today", "usage_frequency": "daily"}), + Subscription(id=uid(), user_id=user.id, merchant="LinkedIn Premium", + amount=39.99, billing_cycle="monthly", active=True, + ai_usage_detection={"last_used": "60 days ago", "usage_frequency": "very_low"}), + ] + db.add_all(subscriptions) + monthly_sub_cost = sum(s.amount for s in subscriptions) + print(f"Created {len(subscriptions)} subscriptions (${monthly_sub_cost:.2f}/month)") + + # ── Notifications ───────────────────────────────────────────────────── + notifications = [ + Notification(id=uid(), user_id=user.id, + title="🚨 Unusual Transaction Detected", + message="A charge of $847.00 at 'Tech Store NYC' was flagged. " + "This is 3.2x your average transaction and occurred at 11:47 PM. " + "Please review and confirm.", + type="alert", read_status=False, + created_at=now - timedelta(hours=2)), + Notification(id=uid(), user_id=user.id, + title="💡 AI Weekly Insight", + message="Your savings rate this month is 38.4% — 18% above the national average. " + "At this pace, you'll reach your Emergency Fund goal in 3 months.", + type="insight", read_status=False, + created_at=now - timedelta(hours=6)), + Notification(id=uid(), user_id=user.id, + title="⚠️ Budget Alert: Shopping", + message="You've spent $847 in Shopping this month — 141% of your $600 budget. " + "Consider pausing non-essential purchases for the rest of the month.", + type="warning", read_status=False, + created_at=now - timedelta(hours=8)), + Notification(id=uid(), user_id=user.id, + title="🎯 Goal Milestone Reached", + message="Your Emergency Fund is now 78.9% funded ($14,200 of $18,000). " + "You're on track to complete it by August 2026.", + type="insight", read_status=True, + created_at=now - timedelta(days=1)), + Notification(id=uid(), user_id=user.id, + title="📊 Monthly Report Ready", + message="Your May 2026 financial report is ready. " + "Net savings: $1,847. Top category: Housing (38%). " + "Health score improved by 3 points to 82/100.", + type="insight", read_status=True, + created_at=now - timedelta(days=2)), + Notification(id=uid(), user_id=user.id, + title="💰 Subscription Optimization", + message="AI detected 2 underused subscriptions: Adobe CC ($54.99/mo, last used 45 days ago) " + "and LinkedIn Premium ($39.99/mo, last used 60 days ago). " + "Cancelling both saves $1,139.76/year.", + type="warning", read_status=True, + created_at=now - timedelta(days=3)), + ] + db.add_all(notifications) + print(f"Created {len(notifications)} notifications ({sum(1 for n in notifications if not n.read_status)} unread)") + + db.commit() + print("\n" + "="*60) + print("DEMO ACCOUNT SEEDED SUCCESSFULLY") + print("="*60) + print(f" Email: {DEMO_EMAIL}") + print(f" Password: {DEMO_PASSWORD}") + print(f" Balance: ${checking.balance + savings.balance + invest.balance:,.2f} total") + print(f" Score: 82/100 (estimated)") + print(f" Fraud: 1 pending alert") + print("="*60) + + except Exception as e: + db.rollback() + print(f"SEED FAILED: {e}") + raise + finally: + db.close() + +if __name__ == "__main__": + seed() diff --git a/backend/app/scripts/test_endpoints.py b/backend/app/scripts/test_endpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..a39adff3b692f328d08b6350452d5c25dccc6fbe --- /dev/null +++ b/backend/app/scripts/test_endpoints.py @@ -0,0 +1,249 @@ +""" +BankBot AI Endpoint Validation Script +====================================== +Calls every AI endpoint and asserts the response shape is correct. + +Usage: + # From the backend/ directory with the server running: + python app/scripts/test_endpoints.py + +Exit codes: + 0 — all tests passed + 1 — one or more tests failed +""" + +import sys +import json +import httpx + +BASE_URL = "http://127.0.0.1:8000" + +# ─── Result tracking ────────────────────────────────────────────────────────── + +results = [] # list of (name, passed, detail) + +def record(name: str, passed: bool, detail: str = ""): + results.append((name, passed, detail)) + +# ─── Helpers ────────────────────────────────────────────────────────────────── + +def get(path: str, params: dict = None): + return httpx.get(f"{BASE_URL}{path}", params=params, timeout=60) + +def post(path: str, body: dict): + return httpx.post(f"{BASE_URL}{path}", json=body, timeout=60) + +def assert_keys(data: dict, *keys): + missing = [k for k in keys if k not in data] + if missing: + raise AssertionError(f"Missing keys: {missing}") + +# ─── Tests ──────────────────────────────────────────────────────────────────── + +def test_health(): + r = get("/health") + assert r.status_code == 200 + assert r.json().get("status") == "healthy" + record("GET /health", True) + +def test_ai_status(): + r = get("/api/ai/status") + assert r.status_code == 200 + data = r.json() + assert_keys(data, "ai_backend", "ai_available", "db_type", "cache_type") + assert data["db_type"] in ("sqlite", "postgresql") + assert data["cache_type"] in ("redis", "memory") + record("GET /api/ai/status", True, + f"backend={data['ai_backend']} db={data['db_type']} cache={data['cache_type']}") + +def test_twin_predict(): + r = get("/api/ai/twin/predict") + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "current_balance", "projected_balance", "percent_change", + "net_daily", "insight", "chart_data") + assert isinstance(data["chart_data"], list) and len(data["chart_data"]) >= 1 + assert data["projected_balance"] >= 0.0, "projected_balance must be non-negative" + record("GET /api/ai/twin/predict", True, + f"balance=${data['current_balance']:,.2f} → ${data['projected_balance']:,.2f}") + +def test_twin_future(): + r = get("/api/ai/twin/future", params={"months": 12}) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "savings_growth", "investment_growth", "debt_decline") + assert len(data["savings_growth"]) >= 1 + assert len(data["investment_growth"]) >= 1 + record("GET /api/ai/twin/future", True, + f"savings_points={len(data['savings_growth'])}") + +def test_twin_scenarios(): + r = get("/api/ai/twin/scenarios", params={"months": 6}) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "status_quo", "frugal", "lifestyle_inflation") + for key in ("status_quo", "frugal", "lifestyle_inflation"): + assert "balance_projection" in data[key], f"Missing balance_projection in {key}" + record("GET /api/ai/twin/scenarios", True) + +def test_simulate_purchase(): + r = post("/api/ai/simulate/purchase", { + "amount": 500.0, "merchant": "Test Store", "category": "Shopping" + }) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "risk_analysis", "projected_balance", "recommendation") + assert data["risk_analysis"]["risk_level"] in ("low", "medium", "high", "critical") + assert data["projected_balance"] >= 0.0 + record("POST /api/ai/simulate/purchase", True, + f"risk={data['risk_analysis']['risk_level']}") + +def test_simulate_investment(): + r = post("/api/ai/simulate/investment", { + "monthly_sip": 200.0, "asset_type": "stock", "lump_sum": 0.0 + }) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "growth_projection", "is_affordable", "risk_analysis") + assert len(data["growth_projection"]) == 3, \ + f"Expected 3 growth milestones (1/3/5 yr), got {len(data['growth_projection'])}" + record("POST /api/ai/simulate/investment", True, + f"affordable={data['is_affordable']}") + +def test_simulate_subscription(): + # First fetch a real subscription ID from the optimize endpoint + r_subs = get("/api/ai/subscriptions/optimize") + assert r_subs.status_code == 200 + subs = r_subs.json().get("subscriptions", []) + if not subs: + record("POST /api/ai/simulate/subscription", True, "skipped — no subscriptions in DB") + return + sub_id = subs[0]["id"] + r = post("/api/ai/simulate/subscription", {"subscription_ids": [sub_id]}) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "monthly_savings", "yearly_savings", "recommendation") + assert data["monthly_savings"] >= 0.0 + record("POST /api/ai/simulate/subscription", True, + f"monthly_savings=${data['monthly_savings']:.2f}") + +def test_behavior_insights(): + r = get("/api/ai/behavior/insights") + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "insights", "metrics") + assert isinstance(data["insights"], list) and len(data["insights"]) >= 1, \ + "insights must be a non-empty list" + record("GET /api/ai/behavior/insights", True, + f"insights={len(data['insights'])}") + +def test_coaching_score(): + r = get("/api/ai/coaching/score") + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "overall_score", "categories", "explanation", "actionable_improvements") + score = data["overall_score"] + assert 0 <= score <= 100, f"overall_score {score} out of [0, 100]" + expected_cats = ("savings_consistency", "debt_ratio", "spending_discipline", + "emergency_funds", "investments", "subscription_management") + for cat in expected_cats: + assert cat in data["categories"], f"Missing category: {cat}" + assert len(data["actionable_improvements"]) >= 1 + record("GET /api/ai/coaching/score", True, f"score={score}/100") + +def test_coaching_briefing(): + # This endpoint calls an LLM — allow up to 120s for local Ollama inference + r = httpx.get(f"{BASE_URL}/api/ai/coaching/briefing", timeout=120) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "date", "user_name", "briefing", "metrics") + assert isinstance(data["briefing"], str) and len(data["briefing"]) > 10 + record("GET /api/ai/coaching/briefing", True, + f"briefing_len={len(data['briefing'])} chars") + +def test_subscriptions_optimize(): + r = get("/api/ai/subscriptions/optimize") + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "subscriptions", "duplicates", "unused_subscriptions", + "yearly_savings_potential", "risk_analysis") + record("GET /api/ai/subscriptions/optimize", True, + f"subs={len(data['subscriptions'])} " + f"dupes={len(data['duplicates'])} " + f"unused={len(data['unused_subscriptions'])}") + +def test_fraud_analysis(): + r = get("/api/ai/fraud/analysis") + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert_keys(data, "total_alerts", "pending_reviews", "alerts") + assert isinstance(data["total_alerts"], int) + record("GET /api/ai/fraud/analysis", True, + f"alerts={data['total_alerts']}") + +def test_chat(): + # This endpoint calls an LLM — allow up to 120s for local Ollama inference + r = httpx.post(f"{BASE_URL}/api/ai/chat", + json={"message": "What is my current savings rate?"}, + timeout=120) + assert r.status_code == 200, f"HTTP {r.status_code}: {r.text}" + data = r.json() + assert "response" in data, "Missing 'response' key" + assert isinstance(data["response"], str) and len(data["response"]) > 5 + record("POST /api/ai/chat", True, + f"response_len={len(data['response'])} chars") + +# ─── Runner ─────────────────────────────────────────────────────────────────── + +TESTS = [ + test_health, + test_ai_status, + test_twin_predict, + test_twin_future, + test_twin_scenarios, + test_simulate_purchase, + test_simulate_investment, + test_simulate_subscription, + test_behavior_insights, + test_coaching_score, + test_coaching_briefing, + test_subscriptions_optimize, + test_fraud_analysis, + test_chat, +] + +if __name__ == "__main__": + print(f"\n{'─'*60}") + print(f" BankBot AI Endpoint Validation — {BASE_URL}") + print(f"{'─'*60}\n") + + for test_fn in TESTS: + name = test_fn.__name__.replace("test_", "").replace("_", " ") + try: + test_fn() + # result already recorded inside test_fn on success + except AssertionError as e: + record(name, False, str(e)) + except Exception as e: + record(name, False, f"Exception: {e}") + + # ── Summary table ───────────────────────────────────────────────────────── + print(f"\n{'─'*60}") + print(f" {'TEST':<40} {'RESULT':<8} DETAIL") + print(f"{'─'*60}") + + passed = 0 + failed = 0 + for test_name, ok, detail in results: + status = "✅ PASS" if ok else "❌ FAIL" + print(f" {test_name:<40} {status:<8} {detail}") + if ok: + passed += 1 + else: + failed += 1 + + print(f"{'─'*60}") + print(f" {passed} passed | {failed} failed | {len(results)} total") + print(f"{'─'*60}\n") + + sys.exit(0 if failed == 0 else 1) diff --git a/backend/app/scripts/test_websocket.py b/backend/app/scripts/test_websocket.py new file mode 100644 index 0000000000000000000000000000000000000000..b3bc48e16ec581cbf21aedb5567d0d0a5627cf3d --- /dev/null +++ b/backend/app/scripts/test_websocket.py @@ -0,0 +1,159 @@ +""" +BankBot WebSocket Streaming Validation Script +============================================== +Tests the /api/ai/chat/ws WebSocket endpoint for: + 1. Streaming chat response (chat_start → chat_chunk(s) → chat_end) + 2. Ping/pong keepalive + 3. Invalid JSON error handling + +Usage: + # From the backend/ directory with the server running: + python app/scripts/test_websocket.py + +Exit codes: + 0 — all tests passed + 1 — one or more tests failed +""" + +import sys +import json +import asyncio +import websockets + +WS_URL = "ws://127.0.0.1:8000/api/ai/chat/ws" + +# ─── Result tracking ────────────────────────────────────────────────────────── + +results = [] # list of (name, passed, detail) + +def record(name: str, passed: bool, detail: str = ""): + results.append((name, passed, detail)) + +# ─── Tests ──────────────────────────────────────────────────────────────────── + +async def test_chat_streaming(): + """ + Sends a chat message and verifies the full streaming protocol: + chat_start → one or more chat_chunk → chat_end + """ + async with websockets.connect(WS_URL, open_timeout=10) as ws: + await ws.send(json.dumps({ + "type": "chat", + "message": "What is my current balance and savings rate?" + })) + + got_start = False + got_chunk = False + got_end = False + full_reply = "" + + # Collect messages with a 30-second timeout + deadline = asyncio.get_event_loop().time() + 30 + while asyncio.get_event_loop().time() < deadline: + try: + raw = await asyncio.wait_for(ws.recv(), timeout=30) + except asyncio.TimeoutError: + break + + msg = json.loads(raw) + t = msg.get("type") + + if t == "chat_start": + got_start = True + elif t == "chat_chunk": + got_chunk = True + full_reply += msg.get("content", "") + elif t == "chat_end": + got_end = True + break + elif t == "error": + raise AssertionError(f"Server returned error: {msg.get('message')}") + + assert got_start, "Never received chat_start" + assert got_chunk, "Never received any chat_chunk" + assert got_end, "Never received chat_end" + assert len(full_reply) > 5, f"Assembled reply is too short: '{full_reply}'" + + record("WS chat streaming", True, + f"reply_len={len(full_reply)} chars | preview: {full_reply[:80].strip()}...") + + +async def test_ping_pong(): + """ + Sends a ping and verifies the server responds with pong. + """ + async with websockets.connect(WS_URL, open_timeout=10) as ws: + await ws.send(json.dumps({"type": "ping"})) + + raw = await asyncio.wait_for(ws.recv(), timeout=10) + msg = json.loads(raw) + + assert msg.get("type") == "pong", \ + f"Expected pong, got: {msg}" + + record("WS ping/pong", True) + + +async def test_invalid_json(): + """ + Sends a non-JSON string and verifies the server returns an error message. + """ + async with websockets.connect(WS_URL, open_timeout=10) as ws: + await ws.send("this is not valid json {{{{") + + raw = await asyncio.wait_for(ws.recv(), timeout=10) + msg = json.loads(raw) + + assert msg.get("type") == "error", \ + f"Expected error response, got: {msg}" + + record("WS invalid JSON handling", True, + f"error_msg={msg.get('message', '')[:60]}") + + +# ─── Runner ─────────────────────────────────────────────────────────────────── + +async def main(): + print(f"\n{'─'*60}") + print(f" BankBot WebSocket Validation — {WS_URL}") + print(f"{'─'*60}\n") + + tests = [ + ("WS chat streaming", test_chat_streaming), + ("WS ping/pong", test_ping_pong), + ("WS invalid JSON handling", test_invalid_json), + ] + + for name, test_fn in tests: + try: + await test_fn() + except AssertionError as e: + record(name, False, str(e)) + except Exception as e: + record(name, False, f"Exception: {type(e).__name__}: {e}") + + # ── Summary table ───────────────────────────────────────────────────────── + print(f"\n{'─'*60}") + print(f" {'TEST':<35} {'RESULT':<8} DETAIL") + print(f"{'─'*60}") + + passed = 0 + failed = 0 + for test_name, ok, detail in results: + status = "✅ PASS" if ok else "❌ FAIL" + print(f" {test_name:<35} {status:<8} {detail}") + if ok: + passed += 1 + else: + failed += 1 + + print(f"{'─'*60}") + print(f" {passed} passed | {failed} failed | {len(results)} total") + print(f"{'─'*60}\n") + + return failed + + +if __name__ == "__main__": + failed_count = asyncio.run(main()) + sys.exit(0 if failed_count == 0 else 1) diff --git a/backend/app/transactions/__init__.py b/backend/app/transactions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/transactions/router.py b/backend/app/transactions/router.py new file mode 100644 index 0000000000000000000000000000000000000000..9b2b061981ecec9fd64e3fec0f1dd0e7c2d2b7e3 --- /dev/null +++ b/backend/app/transactions/router.py @@ -0,0 +1,60 @@ +""" +Transactions router — paginated transaction history with filtering. +""" +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from sqlalchemy import desc + +from app.database.database import get_db +from app.database.models import User, Account, Transaction + +router = APIRouter(prefix="/api/transactions", tags=["Transactions"]) + +def _resolve_user(db: Session, user_id: Optional[str]) -> str: + if user_id: + return user_id + user = db.query(User).first() + if not user: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="No users found.") + return user.id + +@router.get("/") +def get_transactions( + user_id: Optional[str] = None, + page: int = Query(default=1, ge=1), + limit: int = Query(default=20, ge=1, le=100), + category: Optional[str] = None, + type: Optional[str] = None, + db: Session = Depends(get_db), +): + uid = _resolve_user(db, user_id) + account_ids = [a.id for a in db.query(Account).filter(Account.user_id == uid).all()] + + query = db.query(Transaction).filter(Transaction.account_id.in_(account_ids)) + if category: + query = query.filter(Transaction.category == category) + if type: + query = query.filter(Transaction.type == type) + + total = query.count() + transactions = query.order_by(desc(Transaction.timestamp)).offset((page - 1) * limit).limit(limit).all() + + return { + "transactions": [ + { + "id": t.id, + "merchant": t.merchant or "Unknown", + "category": t.category or "Other", + "amount": t.amount if t.type == "credit" else -abs(t.amount), + "type": t.type, + "timestamp": t.timestamp.isoformat() if t.timestamp else None, + "tags": t.tags or [], + } + for t in transactions + ], + "total": total, + "page": page, + "pages": (total + limit - 1) // limit, + } diff --git a/backend/app/websocket/connection_manager.py b/backend/app/websocket/connection_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..c313ea4eebf1b4755918b08c8d947523f34930ad --- /dev/null +++ b/backend/app/websocket/connection_manager.py @@ -0,0 +1,41 @@ +from typing import Dict, List +from fastapi import WebSocket + +class WebSocketConnectionManager: + def __init__(self): + # Maps user_id -> List[WebSocket] + self.active_connections: Dict[str, List[WebSocket]] = {} + + async def connect(self, websocket: WebSocket, user_id: str): + await websocket.accept() + if user_id not in self.active_connections: + self.active_connections[user_id] = [] + self.active_connections[user_id].append(websocket) + print(f"WebSocket client connected for user: {user_id}") + + def disconnect(self, websocket: WebSocket, user_id: str): + if user_id in self.active_connections: + if websocket in self.active_connections[user_id]: + self.active_connections[user_id].remove(websocket) + if not self.active_connections[user_id]: + del self.active_connections[user_id] + print(f"WebSocket client disconnected for user: {user_id}") + + async def send_personal_message(self, message: dict, user_id: str): + if user_id in self.active_connections: + for connection in self.active_connections[user_id]: + try: + await connection.send_json(message) + except Exception as e: + print(f"Error sending message to {user_id}: {e}") + + async def broadcast(self, message: dict): + for user_id, connections in self.active_connections.items(): + for connection in connections: + try: + await connection.send_json(message) + except Exception as e: + print(f"Error broadcasting message: {e}") + +# Global Connection Manager instance +ws_manager = WebSocketConnectionManager() diff --git a/backend/app/websocket/router.py b/backend/app/websocket/router.py new file mode 100644 index 0000000000000000000000000000000000000000..f30c540d3b1b3b82cf46adbf4d174a937a5bc2bd --- /dev/null +++ b/backend/app/websocket/router.py @@ -0,0 +1,142 @@ +""" +WebSocket chat router — real-time streaming AI chat with: +- Prompt injection prevention +- Input sanitization +- Heartbeat/ping support +- Structured error responses +- Observability metrics tracking +""" +import json +import re +from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query +from app.database.database import SessionLocal +from app.database.models import User +from app.websocket.connection_manager import ws_manager +from app.ai.chat import stream_chat_response +from app.middleware.logging import ws_logger, metrics + +router = APIRouter(tags=["WebSockets"]) + +# ─── Prompt injection patterns ──────────────────────────────────────────────── +_INJECTION_PATTERNS = [ + r"ignore\s+(all\s+)?previous\s+instructions", + r"you\s+are\s+now\s+a", + r"forget\s+(everything|all)", + r"new\s+system\s+prompt", + r"disregard\s+(your|all)", + r"act\s+as\s+(if\s+you\s+are|a\s+different)", + r"jailbreak", + r"dan\s+mode", + r"developer\s+mode", + r"<\s*script", + r"javascript:", +] +_INJECTION_RE = re.compile("|".join(_INJECTION_PATTERNS), re.IGNORECASE) + +MAX_MESSAGE_LENGTH = 2000 + +def sanitize_prompt(text: str) -> tuple[str, bool]: + """ + Returns (sanitized_text, is_safe). + Strips control characters, checks for injection patterns. + """ + # Strip null bytes and control characters (keep newlines/tabs) + cleaned = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text) + # Truncate + cleaned = cleaned[:MAX_MESSAGE_LENGTH] + # Check for injection + if _INJECTION_RE.search(cleaned): + return cleaned, False + return cleaned, True + + +@router.websocket("/api/ai/chat/ws") +async def websocket_chat_endpoint( + websocket: WebSocket, + user_id: str = Query(None), +): + db = SessionLocal() + + # Resolve user + if not user_id: + user = db.query(User).first() + if user: + user_id = user.id + else: + await websocket.accept() + await websocket.send_json({ + "type": "error", + "message": "No users found. Run: python app/scripts/seed_demo.py" + }) + await websocket.close() + db.close() + return + + await ws_manager.connect(websocket, user_id) + metrics.ws_connects += 1 + ws_logger.info("WebSocket connected", extra={"user_id": user_id[:8]}) + + try: + while True: + data = await websocket.receive_text() + + try: + payload = json.loads(data) + except json.JSONDecodeError: + await websocket.send_json({"type": "error", "message": "Invalid JSON"}) + continue + + msg_type = payload.get("type", "chat") + + # ── Heartbeat ──────────────────────────────────────────────────── + if msg_type == "ping": + await websocket.send_json({"type": "pong"}) + continue + + # ── Chat message ───────────────────────────────────────────────── + if msg_type == "chat": + raw_prompt = payload.get("message", "").strip() + + if not raw_prompt: + await websocket.send_json({"type": "error", "message": "Message cannot be empty"}) + continue + + # Sanitize + injection check + prompt, is_safe = sanitize_prompt(raw_prompt) + if not is_safe: + ws_logger.warning("Prompt injection attempt blocked", extra={"user_id": user_id[:8]}) + await websocket.send_json({ + "type": "error", + "message": "I can only help with financial questions about your accounts." + }) + continue + + await websocket.send_json({"type": "chat_start"}) + + try: + for chunk in stream_chat_response(db, user_id, prompt): + if chunk: + await websocket.send_json({"type": "chat_chunk", "content": chunk}) + except Exception as e: + ws_logger.error("AI streaming error", extra={"error": str(e)[:100]}) + await websocket.send_json({ + "type": "error", + "message": "AI response failed. Please try again." + }) + + await websocket.send_json({"type": "chat_end"}) + + else: + await websocket.send_json({ + "type": "error", + "message": f"Unknown message type: {msg_type}" + }) + + except WebSocketDisconnect: + ws_manager.disconnect(websocket, user_id) + ws_logger.info("WebSocket disconnected", extra={"user_id": user_id[:8]}) + except Exception as e: + ws_logger.error("WebSocket error", extra={"user_id": user_id[:8], "error": str(e)[:100]}) + ws_manager.disconnect(websocket, user_id) + finally: + db.close() diff --git a/backend/render.yaml b/backend/render.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2761270bb6cac84158f7f0537c3fa0b226e87e20 --- /dev/null +++ b/backend/render.yaml @@ -0,0 +1,37 @@ +services: + - type: web + name: bankbot-api + runtime: python + buildCommand: pip install -r requirements.txt + startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 2 + healthCheckPath: /health + envVars: + - key: DATABASE_URL + fromDatabase: + name: bankbot-db + property: connectionString + - key: REDIS_URL + fromService: + name: bankbot-redis + type: redis + property: connectionString + - key: JWT_SECRET_KEY + generateValue: true + - key: OPENAI_API_KEY + sync: false + - key: GROQ_API_KEY + sync: false + - key: BACKEND_CORS_ORIGINS + value: '["https://bankbot.vercel.app"]' + - key: ACCESS_TOKEN_EXPIRE_MINUTES + value: "60" + +databases: + - name: bankbot-db + databaseName: bankbot + user: bankbot_user + plan: free + + - name: bankbot-redis + type: redis + plan: free diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2bdf2d5d4e189086bbccbfa94b45afac3baaa471 Binary files /dev/null and b/backend/requirements.txt differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..e61ba71431bdedf7594dc502a57e1932dcca512d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,140 @@ +version: "3.8" + +# ============================================================ +# BankBot AI — Docker Compose +# +# Development: docker compose up -d +# Production: docker compose --profile production up -d +# Seed data: docker compose exec backend python app/scripts/seed_demo.py +# Logs: docker compose logs -f backend +# Stop: docker compose down +# ============================================================ + +services: + + # ─── PostgreSQL ───────────────────────────────────────────── + db: + image: postgres:15-alpine + container_name: bankbot_postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-adminpassword} + POSTGRES_DB: ${POSTGRES_DB:-bankbot} + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-admin} -d ${POSTGRES_DB:-bankbot}"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + # ─── Redis ────────────────────────────────────────────────── + redis: + image: redis:7-alpine + container_name: bankbot_redis + restart: unless-stopped + command: > + redis-server + --maxmemory 256mb + --maxmemory-policy allkeys-lru + --save 60 1 + ports: + - "6379:6379" + volumes: + - redisdata:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + # ─── Backend ──────────────────────────────────────────────── + backend: + build: + context: ./backend + dockerfile: Dockerfile + target: runtime + container_name: bankbot_backend + restart: unless-stopped + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD:-adminpassword}@db:5432/${POSTGRES_DB:-bankbot} + REDIS_URL: redis://redis:6379/0 + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + GROQ_API_KEY: ${GROQ_API_KEY:-} + OLLAMA_MODEL: ${OLLAMA_MODEL:-llama3:latest} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-bankbot-change-in-production} + JWT_ALGORITHM: HS256 + ACCESS_TOKEN_EXPIRE_MINUTES: "60" + BACKEND_CORS_ORIGINS: '["http://localhost:3000","http://frontend:3000","${FRONTEND_URL:-http://localhost:3000}"]' + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 20s + + # ─── Frontend ─────────────────────────────────────────────── + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + target: runtime + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + container_name: bankbot_frontend + restart: unless-stopped + ports: + - "3000:3000" + environment: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + NODE_ENV: production + depends_on: + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # ─── Nginx (production profile only) ──────────────────────── + nginx: + image: nginx:1.25-alpine + container_name: bankbot_nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - nginx_logs:/var/log/nginx + depends_on: + - frontend + - backend + profiles: + - production + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 5s + retries: 3 + +volumes: + pgdata: + driver: local + redisdata: + driver: local + nginx_logs: + driver: local diff --git a/docs/API_DOCUMENTATION.md b/docs/API_DOCUMENTATION.md new file mode 100644 index 0000000000000000000000000000000000000000..d74463a392a8e9c751272435132c210ee414d4d0 --- /dev/null +++ b/docs/API_DOCUMENTATION.md @@ -0,0 +1,449 @@ +# BankBot AI — Complete API Documentation + +Base URL: `http://localhost:8000` +WebSocket: `ws://localhost:8000` +API Docs (interactive): `http://localhost:8000/docs` + +--- + +## Authentication + +All protected endpoints require: +``` +Authorization: Bearer +``` + +### POST /api/auth/register +Create a new user account. + +**Request:** +```json +{ + "email": "user@example.com", + "password": "SecurePass123!", + "name": "Alex Doe" +} +``` + +**Response 201:** +```json +{ + "access_token": "eyJhbGci...", + "refresh_token": "eyJhbGci...", + "token_type": "bearer", + "user_id": "uuid", + "name": "Alex Doe", + "email": "user@example.com" +} +``` + +**Errors:** `409` email already registered + +--- + +### POST /api/auth/login +Login with email and password (form-encoded). + +**Request:** `application/x-www-form-urlencoded` +``` +username=alex@bankbot.dev&password=BankBot2026! +``` + +**Response 200:** Same as register response + +**Errors:** `401` incorrect credentials + +--- + +### POST /api/auth/refresh +Exchange a refresh token for a new access token. + +**Request:** +```json +{ "refresh_token": "eyJhbGci..." } +``` + +**Response:** +```json +{ "access_token": "eyJhbGci...", "token_type": "bearer" } +``` + +--- + +### GET /api/auth/me +Get current authenticated user profile. + +**Response:** +```json +{ + "user_id": "uuid", + "email": "alex@bankbot.dev", + "name": "Alex Doe", + "financial_personality": "Balanced Investor" +} +``` + +--- + +### POST /api/auth/logout +Logout (client should discard tokens). + +**Response:** `{ "message": "Logged out successfully" }` + +--- + +## Dashboard + +### GET /api/dashboard/overview +Full dashboard data in a single request. Cached 2 minutes. + +**Query params:** `user_id` (optional, defaults to first user) + +**Response:** +```json +{ + "total_balance": 59637.82, + "accounts": [ + { "id": "uuid", "type": "checking", "balance": 12847.32, "currency": "USD" } + ], + "monthly_income": 5100.00, + "monthly_expenses": 3240.50, + "savings_rate": 36.5, + "spending_by_category": [ + { "name": "Housing", "value": 1950.00 }, + { "name": "Food", "value": 487.30 } + ], + "recent_transactions": [ + { + "id": "uuid", + "merchant": "Whole Foods", + "category": "Groceries", + "amount": -87.43, + "type": "debit", + "timestamp": "2026-05-24T14:32:00" + } + ], + "cash_flow": [ + { "month": "Dec", "income": 5100, "expenses": 3200, "savings": 1900 } + ], + "health_score": 82.0, + "fraud_alert_count": 1, + "ai_briefing": { + "summary": "Your financial health looks strong...", + "briefing": null + } +} +``` + +**Performance:** ~65ms cold, ~10ms cached + +--- + +## AI Intelligence + +### GET /api/ai/coaching/score +Financial health score (0-100) across 6 dimensions. Cached 10 minutes. + +**Response:** +```json +{ + "overall_score": 82.0, + "categories": { + "savings_consistency": { "score": 15, "max": 20 }, + "debt_ratio": { "score": 20, "max": 20 }, + "spending_discipline": { "score": 16, "max": 20 }, + "emergency_funds": { "score": 15, "max": 20 }, + "investments": { "score": 10, "max": 10 }, + "subscription_management": { "score": 6, "max": 10 } + }, + "explanation": "Your portfolio demonstrates strong debt management...", + "actionable_improvements": [ + "Set up automated transfers to your Savings account right after payday." + ] +} +``` + +--- + +### GET /api/ai/coaching/briefing +AI-generated daily financial briefing. Cached 1 hour. + +**Response:** +```json +{ + "date": "2026-05-24", + "user_name": "Alex Doe", + "briefing": "DAILY BRIEFING:\n\nYour liquid capital stands at $59,637...", + "metrics": { + "total_liquid_capital": 59637.82, + "monthly_income_projection": 5100.00, + "monthly_burn_rate": 3240.50 + } +} +``` + +--- + +### GET /api/ai/behavior/insights +Spending behavior analysis. Cached 10 minutes. + +**Response:** +```json +{ + "insights": [ + "Weekend spending is 34% higher than weekday average", + "Late-night transactions detected 3 times this month" + ], + "metrics": { + "weekend_pct": 34.2, + "late_night_count": 3, + "avg_transaction_amount": 67.40 + } +} +``` + +--- + +### GET /api/ai/twin/predict +Balance forecast for next 30 days. Cached 5 minutes. + +**Response:** +```json +{ + "current_balance": 59637.82, + "predicted_balance_30d": 61200.00, + "monthly_projections": [ + { "month": 1, "balance": 61200, "savings": 1562 } + ], + "confidence": "high" +} +``` + +--- + +### GET /api/ai/twin/future +Long-term balance projection. + +**Query params:** `months` (1-60, default 12) + +--- + +### GET /api/ai/twin/scenarios +Conservative / expected / optimistic scenarios. + +**Query params:** `months` (1-24, default 6) + +**Response:** +```json +{ + "scenarios": { + "conservative": [58000, 58500, 59000], + "expected": [60000, 61500, 63000], + "optimistic": [62000, 64500, 67000] + } +} +``` + +--- + +### GET /api/ai/fraud/analysis +All fraud alerts for the user. + +**Response:** +```json +{ + "total_alerts": 1, + "pending_reviews": 1, + "alerts": [ + { + "fraud_log_id": "uuid", + "transaction_id": "uuid", + "amount": 847.00, + "merchant": "Tech Store NYC", + "risk_score": 78.0, + "details": "Amount 3.2x above average. Late-night transaction.", + "status": "pending" + } + ] +} +``` + +--- + +### POST /api/ai/chat +HTTP chat endpoint (fallback when WebSocket unavailable). + +**Request:** +```json +{ "message": "Analyze my spending this month" } +``` + +**Response:** +```json +{ "response": "Your spending this month totals $3,240..." } +``` + +--- + +### POST /api/ai/simulate/purchase +Simulate the impact of a purchase on your finances. + +**Request:** +```json +{ + "amount": 1200.00, + "merchant": "Apple Store", + "category": "Tech" +} +``` + +--- + +## WebSocket Chat + +### WS /api/ai/chat/ws + +Connect: `ws://localhost:8000/api/ai/chat/ws?user_id={id}` + +**Client → Server:** +```json +{ "type": "chat", "message": "What is my savings rate?" } +{ "type": "ping" } +``` + +**Server → Client:** +```json +{ "type": "chat_start" } +{ "type": "chat_chunk", "content": "Your " } +{ "type": "chat_chunk", "content": "savings " } +{ "type": "chat_chunk", "content": "rate is 36.5%..." } +{ "type": "chat_end" } +{ "type": "pong" } +{ "type": "error", "message": "..." } +``` + +**Connection lifecycle:** +- Heartbeat: client sends `ping` every 25s +- Reconnect: exponential backoff (1s, 2s, 4s, 8s, 16s, 30s max) +- HTTP fallback: if WS unavailable, falls back to POST /api/ai/chat + +--- + +## Transactions + +### GET /api/transactions/ +Paginated transaction history. + +**Query params:** +- `page` (default 1) +- `limit` (default 20, max 100) +- `category` (filter by category) +- `type` (credit or debit) +- `user_id` (optional) + +**Response:** +```json +{ + "transactions": [ + { + "id": "uuid", + "merchant": "Whole Foods", + "category": "Groceries", + "amount": -87.43, + "type": "debit", + "timestamp": "2026-05-24T14:32:00", + "tags": ["groceries"] + } + ], + "total": 301, + "page": 1, + "pages": 16 +} +``` + +--- + +## Notifications + +### GET /api/notifications/ +List user notifications. + +**Query params:** `limit` (default 20), `user_id` + +**Response:** +```json +{ + "notifications": [ + { + "id": "uuid", + "title": "Unusual Transaction Detected", + "message": "A charge of $847.00 at Tech Store NYC was flagged...", + "type": "alert", + "read": false, + "created_at": "2026-05-24T22:00:00" + } + ], + "unread_count": 3 +} +``` + +### PATCH /api/notifications/{id}/read +Mark a notification as read. Returns `{ "success": true }` + +### PATCH /api/notifications/read-all +Mark all notifications as read. Returns `{ "success": true }` + +### DELETE /api/notifications/{id} +Dismiss a notification. Returns `{ "success": true }` + +--- + +## Observability + +### GET /health +```json +{ + "status": "healthy", + "timestamp": 1748131200.0, + "db": "sqlite", + "cache": "memory", + "uptime_s": 771.0 +} +``` + +### GET /api/status +```json +{ + "ai_backend": "groq", + "ai_available": true, + "db_type": "sqlite", + "cache_type": "memory", + "version": "2.0.0" +} +``` + +### GET /api/metrics +Live observability dashboard: +```json +{ + "uptime_seconds": 771.0, + "requests": { + "total": 20, + "errors": 0, + "auth_failures": 0, + "error_rate_pct": 0.0 + }, + "websocket": { "total_connects": 0, "reconnects": 0 }, + "ai": { + "fallbacks": 0, + "by_provider": { + "groq": { "calls": 2, "errors": 0, "avg_latency_ms": 1840.5, "p95_latency_ms": null } + } + }, + "cache": { "hits": 12, "misses": 8, "hit_ratio_pct": 60.0 }, + "route_timings": { + "/api/dashboard/overview": { "calls": 4, "avg_ms": 17.3, "max_ms": 50.0 } + }, + "recent_errors": [] +} +``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..a925acbe1a91e0cb180fdab2dafc12d66e4a2bf3 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,274 @@ +# BankBot AI — System Architecture + +## 1. High-Level Architecture + +``` + ┌─────────────────────────────────┐ + │ CLIENT BROWSER │ + │ Next.js 14 (React, TypeScript) │ + │ │ + │ Pages: │ + │ / Dashboard │ + │ /chat AI Assistant (WS) │ + │ /analytics Spending Intel │ + │ /simulator What-If Engine │ + │ /transactions History │ + │ /status Observability │ + └──────────────┬───────────────────┘ + │ HTTPS / WSS + ┌──────────────▼───────────────────┐ + │ NGINX REVERSE PROXY │ + │ • TLS termination │ + │ • Rate limiting (30r/m API) │ + │ • Auth rate limit (10r/m) │ + │ • WebSocket upgrade proxy │ + │ • Static asset caching │ + └──────────┬────────────┬──────────┘ + │ │ + ┌────────────────────▼──┐ ┌──────▼──────────────────┐ + │ FastAPI Backend │ │ Next.js Standalone │ + │ Python 3.11 │ │ Node.js 20 │ + │ Uvicorn (2 workers) │ │ Port 3000 │ + │ Port 8000 │ └──────────────────────────┘ + │ │ + │ Routers: │ + │ /api/auth │ + │ /api/dashboard │ + │ /api/ai/* │ + │ /api/ai/chat/ws (WS) │ + │ /api/transactions │ + │ /api/notifications │ + │ /api/metrics │ + └──────┬────────┬────────┘ + │ │ + ┌────────────▼──┐ ┌──▼──────────────┐ + │ PostgreSQL 15 │ │ Redis 7 │ + │ (Primary DB) │ │ (Cache Layer) │ + │ │ │ │ + │ Tables: │ │ Keys: │ + │ users │ │ dashboard:* │ + │ accounts │ │ ai:coaching:* │ + │ transactions │ │ ai:behavior:* │ + │ goals │ │ ai:twin:* │ + │ investments │ │ ai:subs:* │ + │ subscriptions │ │ │ + │ notifications │ │ TTLs: │ + │ fraud_logs │ │ dashboard: 2min │ + │ ai_insights │ │ score: 10min │ + │ analytics_ │ │ briefing: 1hr │ + │ snapshots │ └──────────────────┘ + └────────────────┘ + │ + ┌────────────▼──────────────────────────┐ + │ AI ORCHESTRATION LAYER │ + │ │ + │ Priority Chain: │ + │ 1. OpenAI (gpt-4o-mini) ← fastest │ + │ ↓ if unavailable │ + │ 2. Groq (llama-3.3-70b) ← free tier │ + │ ↓ if unavailable │ + │ 3. Ollama (llama3:latest) ← local │ + │ ↓ if unavailable │ + │ 4. Rule-based fallback ← always on │ + │ │ + │ Modules: │ + │ • chat.py — contextual chat │ + │ • coaching.py — health score │ + │ • forecasting.py — balance prediction │ + │ • simulation.py — what-if engine │ + │ • fraud.py — anomaly detection │ + │ • behavior.py — spending patterns │ + │ • subscriptions.py — sub optimization │ + └─────────────────────────────────────────┘ +``` + +--- + +## 2. Data Flow — Dashboard Load + +``` +Browser Next.js FastAPI DB/Cache + │ │ │ │ + │── GET / │ │ │ + │ │── fetch /api/ │ │ + │ │ dashboard/ │ │ + │ │ overview │ │ + │ │ │── check cache ──► │ + │ │ │◄── cache miss ── │ + │ │ │── query accounts │ + │ │ │── query txns │ + │ │── JSON response ◄──│── query fraud │ + │◄── render dashboard ──│ │── set cache(2min) │ + │ │ │ │ + │ [2nd request] │ │ │ + │ │── fetch /api/ │ │ + │ │ dashboard/ │ │ + │ │ overview │ │ + │ │ │── check cache ──► │ + │ │ │◄── cache HIT ─── │ + │◄── render (22ms) ─────│◄── JSON (22ms) ────│ │ +``` + +--- + +## 3. Data Flow — WebSocket Chat + +``` +Browser FastAPI AI Backend + │ │ │ + │── WS connect ─────────► │ + │◄── WS accepted ────────│ │ + │ │ │ + │── { type: "chat", │ │ + │ message: "..." } ──►│ │ + │ │── build context ──►│ + │ │ (user profile, │ + │ │ history, goals) │ + │ │ │── stream tokens + │◄── { type: "chat_start" } │ + │◄── { type: "chat_chunk", content: "He" } │ + │◄── { type: "chat_chunk", content: "re" } │ + │◄── { type: "chat_chunk", content: " is" } │ + │ ... (streaming) │ + │◄── { type: "chat_end" } │ + │ │ │ + │── { type: "ping" } ───►│ (heartbeat 25s) │ + │◄── { type: "pong" } ───│ │ +``` + +--- + +## 4. AI Context Construction + +Every chat message is enriched with full user financial context: + +```python +system_prompt = f""" +You are BankBot, an elite AI Financial Analyst. + +CURRENT USER PORTFOLIO: +- Name: {user.name} +- Financial Personality: {user.financial_personality} +- Health Score: {score}/100 +- Total Balance: ${total_balance:,.2f} +- Accounts: {account_details} +- Goals: {goals_details} +- Investments: {investments_details} +- Subscriptions: {subs_details} +- Behavioral Insights: {behavior_insights} + +PRINCIPLES: +1. Never give generic advice — use real numbers +2. Respond like a Bloomberg Terminal analyst +3. Keep answers brief, actionable, financially meaningful +""" +``` + +--- + +## 5. Fraud Detection Algorithm + +``` +Transaction received + │ + ▼ +┌───────────────────────────────┐ +│ Load last 30 transactions │ +│ for this user │ +└───────────────┬───────────────┘ + │ + ┌───────▼────────┐ + │ Amount spike? │ > 3.5x avg → +40 pts + │ │ > 2.0x avg → +20 pts + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Timing anomaly?│ 11PM–4AM → +25 pts + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Rapid fire? │ < 3 min gap → +20 pts + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Duplicate? │ Same merchant+amount + │ │ within 10 min → +30 pts + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Score ≥ 30? │ → Log to fraud_logs + │ Score ≥ 50? │ → Status: "flagged" + │ Score < 30? │ → Status: "verified" + └────────────────┘ +``` + +--- + +## 6. Caching Strategy + +| Data | Cache Key | TTL | Reason | +|------|-----------|-----|--------| +| Dashboard overview | `dashboard:overview:{uid}` | 2 min | High-frequency, DB-heavy | +| AI health score | `ai:coaching:score:{uid}` | 10 min | AI call expensive | +| AI daily briefing | `ai:coaching:briefing:{uid}` | 1 hr | LLM cost control | +| Behavior insights | `ai:behavior:insights:{uid}` | 10 min | Computation heavy | +| Twin prediction | `ai:twin:predict:{uid}` | 5 min | Moderate cost | +| Subscriptions | `ai:subs:optimize:{uid}` | 10 min | Stable data | + +Cache backend: Redis → in-memory dict fallback (automatic, no config needed). + +--- + +## 7. Security Architecture + +``` +Request → Nginx (rate limit) → FastAPI middleware stack: + 1. Rate limiter (120 req/min per IP) + 2. Security headers (X-Frame-Options, CSP, etc.) + 3. Request logger (structured JSON) + 4. Process time header + 5. CORS validation + 6. Route handler + └── JWT validation (if protected route) + └── Business logic + └── DB query / AI call / Cache lookup +``` + +**JWT Flow:** +``` +Login → access_token (60min) + refresh_token (7 days) + │ + ▼ +Request with Authorization: Bearer {access_token} + │ + ▼ +Token expired? → POST /api/auth/refresh with refresh_token + │ + ▼ +New access_token issued (refresh_token unchanged) + │ + ▼ +Logout → client clears tokens (stateless) +``` + +--- + +## 8. Deployment Architecture + +``` +Internet + │ + ▼ +Cloudflare (DNS + DDoS protection) + │ + ▼ +Nginx (SSL termination, rate limiting) + │ + ├──► Next.js Frontend (Vercel / Docker port 3000) + │ + └──► FastAPI Backend (Render / Docker port 8000) + │ + ├──► PostgreSQL (Render managed / Docker) + ├──► Redis (Render managed / Docker) + └──► AI Provider (OpenAI API / Groq API) +``` diff --git a/docs/DEPLOYMENT_GUIDE.md b/docs/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..f562462b21e99c0228b6a8ebf59b88b72ca4c056 --- /dev/null +++ b/docs/DEPLOYMENT_GUIDE.md @@ -0,0 +1,219 @@ +# BankBot AI — Deployment Guide + +## Option 1: Local Development (Fastest) + +```bash +# 1. Clone and setup backend +cd backend +python -m venv venv +venv\Scripts\activate # Windows +pip install -r requirements.txt +copy .env.example .env # Edit with your API keys + +# 2. Seed demo data +python app/scripts/seed_demo.py + +# 3. Start backend +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + +# 4. In a new terminal — setup frontend +cd frontend +npm install --legacy-peer-deps +npm run dev + +# Access: http://localhost:3000 +# Login: alex@bankbot.dev / BankBot2026! +# API Docs: http://localhost:8000/docs +# Metrics: http://localhost:8000/api/metrics +``` + +--- + +## Option 2: Docker Compose (Recommended for Demo) + +```bash +# 1. Configure environment +cp .env.example .env +# Edit .env — set OPENAI_API_KEY or GROQ_API_KEY + +# 2. Start all services (PostgreSQL + Redis + Backend + Frontend) +docker compose up -d + +# 3. Seed demo data +docker compose exec backend python app/scripts/seed_demo.py + +# 4. Access +# Frontend: http://localhost:3000 +# Backend: http://localhost:8000 +# API Docs: http://localhost:8000/docs + +# 5. View logs +docker compose logs -f backend +docker compose logs -f frontend + +# 6. Stop +docker compose down +``` + +### With Nginx (Production mode) + +```bash +docker compose --profile production up -d +# Access via http://localhost (port 80) +``` + +--- + +## Option 3: Cloud Deployment + +### Frontend → Vercel + +```bash +cd frontend + +# Install Vercel CLI +npm i -g vercel + +# Deploy +vercel --prod + +# Set environment variable in Vercel dashboard: +# NEXT_PUBLIC_API_URL = https://your-backend.onrender.com +``` + +### Backend → Render + +1. Push code to GitHub +2. Go to https://render.com → New → Web Service +3. Connect your GitHub repo +4. Render auto-detects `render.yaml` in `backend/` +5. Set environment variables in Render dashboard: + - `OPENAI_API_KEY` or `GROQ_API_KEY` + - `JWT_SECRET_KEY` (generate a strong random string) +6. Render provisions PostgreSQL and Redis automatically + +### Backend → Railway + +```bash +# Install Railway CLI +npm i -g @railway/cli +railway login + +cd backend +railway init +railway up + +# Add PostgreSQL and Redis plugins in Railway dashboard +# Set environment variables in Railway dashboard +``` + +### Backend → DigitalOcean App Platform + +1. Create new App → GitHub repo +2. Set source directory: `backend` +3. Build command: `pip install -r requirements.txt` +4. Run command: `uvicorn app.main:app --host 0.0.0.0 --port $PORT` +5. Add PostgreSQL and Redis managed databases +6. Set environment variables + +--- + +## Environment Variables Reference + +### Backend (Required for Production) + +```env +# REQUIRED +JWT_SECRET_KEY= +DATABASE_URL=postgresql://user:pass@host:5432/bankbot + +# REQUIRED (at least one AI key) +OPENAI_API_KEY=sk-... +# OR +GROQ_API_KEY=gsk_... + +# RECOMMENDED +REDIS_URL=redis://host:6379/0 +BACKEND_CORS_ORIGINS=["https://your-frontend.vercel.app"] +ACCESS_TOKEN_EXPIRE_MINUTES=60 +``` + +### Frontend (Required for Production) + +```env +NEXT_PUBLIC_API_URL=https://your-backend.onrender.com +``` + +--- + +## Post-Deployment Checklist + +``` +[ ] Backend health check passes: GET /health → {"status": "healthy"} +[ ] API status shows correct backend: GET /api/status +[ ] Demo account works: POST /api/auth/login +[ ] Dashboard loads: GET /api/dashboard/overview +[ ] WebSocket connects: ws://your-backend/api/ai/chat/ws +[ ] Metrics endpoint works: GET /api/metrics +[ ] Frontend loads at production URL +[ ] CORS allows frontend origin +[ ] JWT tokens work end-to-end +[ ] Seed demo data: python app/scripts/seed_demo.py +``` + +--- + +## Troubleshooting + +### Backend won't start +```bash +# Check Python version (needs 3.11+) +python --version + +# Check if port is in use +netstat -ano | findstr :8000 + +# Check logs +uvicorn app.main:app --port 8000 --log-level debug +``` + +### Frontend can't reach backend +```bash +# Check NEXT_PUBLIC_API_URL in .env.local +cat frontend/.env.local + +# Test backend directly +curl http://localhost:8000/health + +# Check CORS — backend must allow frontend origin +# Edit BACKEND_CORS_ORIGINS in .env +``` + +### WebSocket not connecting +```bash +# Check browser console for WS errors +# Verify backend is running on correct port +# Check Nginx config if using reverse proxy (ws:// upgrade headers) +``` + +### AI responses not working +```bash +# Check which backend is active +curl http://localhost:8000/api/status + +# If ai_available: false, check your API keys +# For Ollama: ensure it's running with: ollama serve +# For Groq: verify key at https://console.groq.com +``` + +### Database issues +```bash +# Force SQLite (no PostgreSQL needed) +# In .env: USE_SQLITE=true + +# Re-seed database +python app/scripts/seed_demo.py + +# Check DB type +curl http://localhost:8000/api/status | python -m json.tool +``` diff --git a/docs/ER_DIAGRAM.md b/docs/ER_DIAGRAM.md new file mode 100644 index 0000000000000000000000000000000000000000..ce75b84f4671a0cdbc2e33c937cefecdf7e2e3e3 --- /dev/null +++ b/docs/ER_DIAGRAM.md @@ -0,0 +1,133 @@ +# BankBot AI — Entity Relationship Diagram + +## ER Diagram (Text Notation) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ USERS │ +│ PK id VARCHAR UUID │ +│ email VARCHAR UNIQUE NOT NULL │ +│ password_hash VARCHAR NOT NULL (bcrypt, rounds=12) │ +│ profile_data JSON {name, phone, avatar, plan} │ +│ financial_personality VARCHAR (Saver/Investor/Balanced/...) │ +│ ai_personalization_settings JSON │ +│ created_at TIMESTAMP │ +│ updated_at TIMESTAMP │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ 1 + ┌────────────────────┼────────────────────────────┐ + │ N │ N │ N +┌─────────▼──────────┐ ┌──────▼──────────────┐ ┌──────────▼──────────┐ +│ ACCOUNTS │ │ SUBSCRIPTIONS │ │ GOALS │ +│ PK id UUID │ │ PK id UUID │ │ PK id UUID │ +│ FK user_id │ │ FK user_id │ │ FK user_id │ +│ type VARCHAR │ │ merchant VARCHAR │ │ title VARCHAR │ +│ (checking/ │ │ amount FLOAT │ │ target_amount │ +│ savings/ │ │ billing_cycle │ │ current_amount │ +│ investment) │ │ active BOOLEAN │ │ target_date │ +│ balance FLOAT │ │ ai_usage_ │ │ ai_generated_ │ +│ currency VARCHAR│ │ detection JSON │ │ plan JSON │ +│ status VARCHAR │ └─────────────────────-─┘ └──────────────────-──┘ +└─────────┬───────────┘ + │ 1 + │ N +┌─────────▼──────────────────────────────────────────────────────────┐ +│ TRANSACTIONS │ +│ PK id VARCHAR UUID │ +│ FK account_id → ACCOUNTS.id │ +│ amount FLOAT NOT NULL │ +│ type VARCHAR (credit / debit) │ +│ category VARCHAR (Food/Shopping/Income/...) │ +│ merchant VARCHAR │ +│ timestamp TIMESTAMP │ +│ tags JSON [] │ +│ ai_generated_metadata JSON {} │ +│ spending_emotion_label VARCHAR (impulsive/planned/recurring) │ +└──────────────────────────────┬─────────────────────────────────────┘ + │ 1 + │ 0..1 + ┌──────────▼──────────────┐ + │ FRAUD_LOGS │ + │ PK id UUID │ + │ FK transaction_id │ + │ risk_score FLOAT │ + │ (0.0 – 1.0) │ + │ suspicious_activity_ │ + │ details TEXT │ + │ status VARCHAR │ + │ (pending/resolved/ │ + │ false_positive) │ + └───────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ INVESTMENTS │ +│ PK id VARCHAR UUID │ +│ FK user_id → USERS.id │ +│ asset_name VARCHAR (S&P 500, AAPL, BTC, ...) │ +│ type VARCHAR (stock/crypto/mutual_fund/bond) │ +│ amount_invested FLOAT │ +│ current_value FLOAT │ +│ portfolio_allocation FLOAT (percentage) │ +│ ai_risk_analysis JSON {risk, expected_return, rec} │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ NOTIFICATIONS │ +│ PK id VARCHAR UUID │ +│ FK user_id → USERS.id │ +│ title VARCHAR NOT NULL │ +│ message TEXT NOT NULL │ +│ type VARCHAR (alert/insight/warning) │ +│ read_status BOOLEAN DEFAULT false │ +│ created_at TIMESTAMP │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ AI_INSIGHTS │ +│ PK id VARCHAR UUID │ +│ FK user_id → USERS.id │ +│ type VARCHAR (recommendation/briefing/cashflow) │ +│ content TEXT NOT NULL │ +│ created_at TIMESTAMP │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ ANALYTICS_SNAPSHOTS │ +│ PK id VARCHAR UUID │ +│ FK user_id → USERS.id │ +│ date TIMESTAMP NOT NULL │ +│ total_balance FLOAT │ +│ total_spending FLOAT │ +│ total_savings FLOAT │ +│ financial_score FLOAT │ +│ trends_json JSON │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Relationships Summary + +| From | To | Type | Description | +|------|----|------|-------------| +| USERS | ACCOUNTS | 1:N | User has multiple bank accounts | +| USERS | SUBSCRIPTIONS | 1:N | User has multiple subscriptions | +| USERS | GOALS | 1:N | User has multiple financial goals | +| USERS | INVESTMENTS | 1:N | User has multiple investments | +| USERS | NOTIFICATIONS | 1:N | User receives notifications | +| USERS | AI_INSIGHTS | 1:N | User has AI-generated insights | +| USERS | ANALYTICS_SNAPSHOTS | 1:N | Daily financial snapshots | +| ACCOUNTS | TRANSACTIONS | 1:N | Account has many transactions | +| TRANSACTIONS | FRAUD_LOGS | 1:0..1 | Transaction may have one fraud log | + +## Indexes (Performance) + +```sql +-- Primary lookup patterns +CREATE INDEX idx_transactions_account_id ON transactions(account_id); +CREATE INDEX idx_transactions_timestamp ON transactions(timestamp DESC); +CREATE INDEX idx_transactions_category ON transactions(category); +CREATE INDEX idx_fraud_logs_transaction ON fraud_logs(transaction_id); +CREATE INDEX idx_notifications_user_read ON notifications(user_id, read_status); +CREATE INDEX idx_accounts_user_id ON accounts(user_id); +CREATE INDEX idx_goals_user_id ON goals(user_id); +CREATE INDEX idx_investments_user_id ON investments(user_id); +``` diff --git a/docs/PPT_STRUCTURE.md b/docs/PPT_STRUCTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..9df7e87efd540ac606fb5f7a10784988b5fb1e08 --- /dev/null +++ b/docs/PPT_STRUCTURE.md @@ -0,0 +1,233 @@ +# BankBot AI — Presentation Structure (12 Slides) + +--- + +## Slide 1 — Title Slide +**BankBot AI** +*An AI-Native Financial Operating System* + +Subtitle: Production-grade · Real-time · Intelligent + +Visual: Dark glassmorphism background, animated gradient orb, tech badges + +--- + +## Slide 2 — Problem Statement +**Traditional banking apps are passive. BankBot is intelligent.** + +| Problem | BankBot Solution | +|---------|-----------------| +| Banks show data, not insights | AI-powered financial analysis | +| No real-time fraud intelligence | Live anomaly detection scoring | +| No personalized coaching | Financial health score + AI briefing | +| No scenario planning | What-If Simulator with 36-month projection | +| No behavioral analysis | Spending heatmap + pattern detection | + +Visual: Side-by-side comparison screenshot + +--- + +## Slide 3 — Solution Overview +**Your AI Financial Twin** + +5 core capabilities: +1. Real-time AI chat with full financial context +2. Predictive balance forecasting (conservative/expected/optimistic) +3. Automated fraud detection (4-factor scoring) +4. Behavioral spending analysis (heatmap, patterns) +5. Interactive what-if financial simulator + +Visual: Dashboard screenshot + +--- + +## Slide 4 — System Architecture +**Production-Grade Full-Stack Architecture** + +``` +Next.js 14 Frontend + ↕ HTTPS / WSS +Nginx (TLS + Rate Limiting) + ↕ +FastAPI Backend (33 routes) + ↕ ↕ ↕ +PostgreSQL 15 Redis 7 AI Engine +→ SQLite → memory OpenAI → Groq + fallback fallback → Ollama → offline +``` + +Key point: **Every layer has a fallback — the system never fully fails** + +--- + +## Slide 5 — AI Intelligence Engine +**4-Tier AI Fallback Chain** + +``` +Priority 1: OpenAI GPT-4o-mini (fastest, most capable) + ↓ if unavailable +Priority 2: Groq llama-3.3-70b (free tier, very fast) + ↓ if unavailable +Priority 3: Local Ollama llama3 (fully offline) + ↓ if unavailable +Priority 4: Rule-based engine (always available) +``` + +Context injected per message: +- Live account balances · Transaction history +- Financial goals · Investment portfolio +- Behavioral patterns · Health score + +Visual: Chat page with streaming animation + +--- + +## Slide 6 — Real-Time WebSocket Architecture +**Streaming AI + Live Updates** + +``` +Browser ──WS connect──► FastAPI +Browser ──{ type: "chat", message: "..." }──► + ◄──{ type: "chat_start" }── + ◄──{ type: "chat_chunk", content: "H" }── + ◄──{ type: "chat_chunk", content: "er" }── + ◄──{ type: "chat_end" }── +Browser ──{ type: "ping" }──► (every 25s) + ◄──{ type: "pong" }── +``` + +Features: +- Character-by-character streaming +- Heartbeat every 25s +- Exponential backoff reconnect (1s → 2s → 4s → 30s max) +- HTTP fallback when WebSocket unavailable +- Prompt injection prevention (9 regex patterns) + +--- + +## Slide 7 — Financial Intelligence +**AI-Powered Analytics** + +- **Health Score** — 100-point composite (6 dimensions) +- **Spending Heatmap** — weekly activity patterns +- **Category Intelligence** — AI insights per spending category +- **Net Worth Timeline** — balance trajectory +- **Behavioral Analysis** — late-night spending, weekend patterns +- **Subscription Optimization** — detect unused subscriptions + +Visual: Analytics page screenshot + +--- + +## Slide 8 — Fraud Detection Algorithm +**Real-Time Anomaly Scoring** + +``` +Transaction → Score 4 factors: + Amount spike > 3.5x avg → +40 pts + Timing anomaly 11PM–4AM → +25 pts + Rapid-fire < 3min gap → +20 pts + Duplicate same+10min → +30 pts + +Score ≥ 30 → logged to fraud_logs +Score ≥ 50 → status: "flagged" +Score < 30 → status: "verified" +``` + +Visual: Fraud alert notification screenshot + +--- + +## Slide 9 — Performance & Caching +**Cache-Aside Pattern with Auto-Fallback** + +| Endpoint | Cold | Cached | TTL | +|----------|------|--------|-----| +| Dashboard | 65ms | 10ms | 2 min | +| AI Score | ~2s | 10ms | 10 min | +| AI Briefing | ~3s | 10ms | 1 hr | +| Transactions | 18ms | — | — | + +Optimization: `Query.with_entities()` — column-only queries, no ORM hydration +Result: **32x speedup** (2.1s → 65ms) + +Cache: Redis → in-memory fallback (automatic, zero config) + +--- + +## Slide 10 — Database Design +**10-Table Normalized Schema** + +Core tables: +- `users` — profile, personality, AI settings +- `accounts` — checking/savings/investment +- `transactions` — 300+ with categories, tags, emotion labels +- `goals` — target amounts, AI-generated plans +- `investments` — portfolio with AI risk analysis +- `subscriptions` — with AI usage detection +- `notifications` — typed alerts (fraud/insight/warning) +- `fraud_logs` — risk scores, details, status +- `ai_insights` — cached AI-generated content +- `analytics_snapshots` — daily financial snapshots + +Fallback: PostgreSQL → SQLite (automatic, same ORM code) + +--- + +## Slide 11 — Security & Observability +**Production-Grade Engineering** + +Security: +- JWT (60min access + 7-day refresh rotation) +- bcrypt hashing (rounds=12, direct library) +- Rate limiting (120/min API, 10/min auth) +- Security headers (CSP, X-Frame-Options, etc.) +- Prompt injection prevention (9 patterns) +- CORS restricted to configured origins + +Observability (`GET /api/metrics`): +- Request count, error rate, auth failures +- AI provider health (calls/errors/latency) +- Cache hit ratio +- Per-route timing (avg + max) +- Last 50 errors with timestamps + +CI/CD: GitHub Actions (backend lint, frontend build, Docker smoke test) + +--- + +## Slide 12 — Demo + Conclusion +**Live Demo** + +Demo flow (5 min): +1. Login → Dashboard (65ms load) +2. AI Chat → WebSocket streaming +3. What-If Simulator → live sliders +4. Analytics → heatmap + radar +5. System Status → live metrics + +**What makes this different:** +- Not a prototype — deployable to production today +- Every feature backed by real data (301 transactions) +- AI that knows your finances, not generic advice +- Resilient architecture that never fully fails +- Full observability — you can see it working + +**Numbers:** +33 routes · 14 pages · 10 DB tables · 65ms dashboard · 4-tier AI fallback + +--- + +## Presenter Notes + +**Opening line:** +> "Most banking apps show you data. BankBot understands it." + +**Closing line:** +> "This isn't a student project that happens to use an AI API. It's a production-grade system where AI is the core — every response is personalized, every insight is grounded in real data, and every layer has a fallback." + +**If asked about deployment:** +> "The frontend is on Vercel, the backend on Render with managed PostgreSQL and Redis. The whole stack can also run locally with a single command — `run.bat` on Windows." + +**If asked about the most impressive feature:** +> "The AI orchestration layer. It builds a personalized system prompt from the user's live database records, streams the response through whichever AI provider is available, and falls back gracefully through 4 levels. Very few projects implement this kind of resilient AI infrastructure." diff --git a/docs/VIVA_NOTES.md b/docs/VIVA_NOTES.md new file mode 100644 index 0000000000000000000000000000000000000000..e62ac800daf0aa2a6538d7ded3980bf281194781 --- /dev/null +++ b/docs/VIVA_NOTES.md @@ -0,0 +1,146 @@ +# BankBot AI — Viva / Presentation Notes + +## Elevator Pitch (30 seconds) + +> "BankBot is a production-grade AI financial platform. It gives users a real-time intelligent view of their finances through a multi-provider AI engine — OpenAI, Groq, and local Ollama — with live WebSocket streaming, fraud detection, financial forecasting, and a premium glassmorphism UI. Every layer has a fallback so the system never fully fails. It's containerized, deployable to cloud platforms, and designed to feel like a real fintech product." + +--- + +## Key Numbers (Memorize These) + +| Metric | Value | +|--------|-------| +| API routes | 33 | +| Frontend pages | 14 | +| Database tables | 10 | +| Demo transactions | 301 | +| Dashboard cold | **65ms** | +| Dashboard cached | **10ms** | +| All endpoints (warm) | **< 20ms** | +| AI fallback levels | 4 | +| JWT access TTL | 60 minutes | +| JWT refresh TTL | 7 days | +| Cache TTLs | 2min / 10min / 1hr | +| Rate limit | 120 req/min per IP | +| Auth rate limit | 10 req/min per IP | +| WebSocket heartbeat | 25 seconds | +| bcrypt rounds | 12 | +| Injection patterns blocked | 9 regex patterns | +| CI/CD | GitHub Actions (3 jobs) | + +--- + +## Demo Script (5 minutes) + +### 1. Login (30s) +- Open http://localhost:3000 +- Login: `alex@bankbot.dev` / `BankBot2026!` +- Point out: JWT stored in localStorage, auto-refresh on expiry + +### 2. Dashboard (45s) +- Show: real balance $59,637, cash flow chart, AI briefing banner +- "This loads in 65ms cold — single optimized DB query, no AI blocking" +- Show fraud alert banner at bottom — "1 pending alert" +- Open DevTools Network tab — show the 65ms response + +### 3. AI Chat (90s) +- Navigate to `/chat` +- Show animated AI orb, connection status badge (Live) +- Type: **"Analyze my spending this month"** +- Point out: character-by-character streaming via WebSocket +- Type: **"What's my biggest financial risk?"** +- "The AI has full context — balance, goals, investments, behavior patterns injected into every prompt" +- Show the HTTP fallback: disconnect network → message still sends via POST + +### 4. What-If Simulator (45s) +- Navigate to `/simulator` +- Show AI scenario cards (Conservative / Balanced / Optimistic) — "from real backend forecasting" +- Move Savings Rate slider from 40% → 55% +- "Chart updates instantly — local projection engine, no API call needed" +- Show AI insight updating dynamically below + +### 5. Analytics (30s) +- Navigate to `/analytics` +- Show cash flow bar chart — real DB data +- Switch to Categories — spending breakdown with AI insights +- Switch to Net Worth — trajectory over time + +### 6. Transactions (20s) +- Navigate to `/transactions` +- Show 301 transactions, pagination +- Filter by "Expenses" — instant filter +- Search "Amazon" — client-side search + +### 7. System Status (30s) +- Navigate to `/status` +- Show live metrics: uptime, request count, cache hit ratio, route timings +- "This is a real observability dashboard — not mocked data" +- Point out: dashboard at 17ms avg, metrics at 2ms + +--- + +## Technical Q&A (15 Most Likely Questions) + +### Q1: Why FastAPI over Django/Flask? +**A:** FastAPI gives us async support, automatic OpenAPI docs at `/docs`, Pydantic validation, and native WebSocket support — all critical for a real-time AI platform. It's also 2-3x faster than Flask for I/O-bound workloads due to async/await. + +### Q2: How does the AI fallback chain work? +**A:** At startup, the system checks for API keys. If `OPENAI_API_KEY` exists → GPT-4o-mini. If not → Groq's llama-3.3-70b (free tier, very fast). If neither → local Ollama (fully offline). If Ollama isn't running → rule-based responses derived from the user's actual database records. The user always gets a response — the system never returns an error to the user. + +### Q3: Why WebSocket for chat instead of HTTP streaming? +**A:** WebSocket gives us bidirectional communication — the server can push fraud alerts, balance updates, and AI responses without the client polling. HTTP SSE is one-directional. WebSocket also supports our heartbeat/ping system (every 25s) for connection health monitoring, and enables future push notifications. + +### Q4: How is the dashboard so fast (65ms)? +**A:** Three optimizations: (1) `Query.with_entities()` — selects only needed columns, no full ORM object hydration. (2) Single query for 6-month cash flow instead of 6 sequential queries. (3) Dashboard never calls AI inline — reads from cache only. Result: 65ms cold, 10ms cached. + +### Q5: How does fraud detection work? +**A:** Rule-based scoring across 4 dimensions: amount spike (>3.5x average = +40pts), timing anomaly (11PM-4AM = +25pts), rapid-fire transactions (<3min gap = +20pts), duplicate detection (same merchant+amount within 10min = +30pts). Score ≥30 logs to `fraud_logs`, ≥50 flags as high-risk. No ML model needed — deterministic and explainable. + +### Q6: What's the financial health score? +**A:** A 100-point composite across 6 dimensions: Savings Consistency (20pts), Debt Ratio (20pts), Spending Discipline (20pts), Emergency Fund (20pts), Investment Index (10pts), Subscription Efficiency (10pts). Each calculated from real DB records. An AI explanation is generated and cached for 10 minutes to control LLM costs. + +### Q7: How is authentication secured? +**A:** JWT with two tokens — 60-minute access token and 7-day refresh token. Passwords hashed with bcrypt (rounds=12) using the `bcrypt` library directly — not passlib, which has a known incompatibility with bcrypt>=4. Rate limiting on auth: 10 requests/minute per IP. Auto-refresh on 401 in the frontend API client. + +### Q8: Why SQLite fallback? +**A:** For development and demo — no PostgreSQL setup required. The fallback is automatic: if PostgreSQL connection fails, the app switches to SQLite. Same ORM code, same queries, zero code changes. This makes the project runnable on any machine with just Python. + +### Q9: How does the What-If Simulator work? +**A:** Two layers: (1) Local projection engine in the browser — instant feedback as sliders move, using compound interest math. No API call, no latency. (2) Real AI scenarios from `/api/ai/twin/scenarios` — the backend runs forecasting algorithms on actual transaction history to generate conservative/expected/optimistic projections. + +### Q10: What is prompt injection and how do you prevent it? +**A:** Prompt injection is when a user tries to override the AI's system prompt with instructions like "ignore all previous instructions". We prevent it with 9 regex patterns that detect common injection phrases. Flagged messages return a safe error response and are logged. The sanitizer also strips control characters and truncates to 2000 characters. + +### Q11: How does the cache-aside pattern work? +**A:** On every request, check cache first. If hit → return cached data (10ms). If miss → query DB, compute result, store in cache with TTL, return result. Redis is primary; if Redis is unavailable, an in-memory dict with TTL tracking is used automatically. No configuration needed — the fallback is transparent. + +### Q12: What makes this production-grade? +**A:** Multi-stage Docker builds with non-root users. Nginx reverse proxy with WebSocket support and rate limiting. Structured JSON logging with request tracing. Live observability dashboard. Security headers on all responses. CORS restricted to configured origins. Graceful degradation at every layer. GitHub Actions CI/CD. Health checks on all Docker services. + +### Q13: How would this scale to 10,000 users? +**A:** Horizontal scaling via Docker Swarm or Kubernetes — stateless JWT means any backend instance handles any request. Redis handles shared cache state across instances. PostgreSQL connection pooling via PgBouncer. The AI calls are already thread-safe with timeout guards. The WebSocket manager would need to move to Redis pub/sub for multi-instance support. + +### Q14: What's the biggest technical challenge you solved? +**A:** The dashboard was timing out at 2+ seconds because it called the AI synchronously on every request. I fixed it by: (1) separating AI generation from data retrieval — dashboard reads from cache only, (2) replacing full ORM object loading with column-only queries using `with_entities()`, (3) caching the fraud count separately. This dropped response time from 2.1s to 65ms — a 32x improvement. + +### Q15: What would you add with more time? +**A:** Real-time WebSocket notifications for fraud alerts (currently polling). Token blacklisting on logout using Redis. End-to-end tests with Playwright. Plaid API integration for live bank transaction data. Mobile app with React Native. Multi-currency support. Budget planning with ML-based category prediction. + +--- + +## Architecture Talking Points + +### "Why is this impressive?" +1. **AI Provider Fallback Chain** — 4 levels, always returns a response +2. **Real-time WebSocket Streaming** — with reconnect, heartbeat, HTTP fallback +3. **Financial Twin** — AI has full user context injected per message +4. **Cache-Aside Pattern** — Redis + memory fallback, automatic +5. **Observability** — live metrics, structured logging, request tracing +6. **Security** — JWT rotation, bcrypt, rate limiting, prompt injection prevention +7. **Performance** — 65ms dashboard, 10ms cached, all endpoints < 20ms warm + +### "What's the most technically advanced part?" +The AI orchestration layer — `stream_chat_response()` builds a personalized system prompt from the user's live database records (balance, goals, investments, behavior), then streams the response through whichever AI provider is available, with automatic fallback and in-memory chat history management. + +### "How is this different from a CRUD app?" +A CRUD app stores and retrieves data. BankBot analyzes it, predicts from it, detects anomalies in it, and explains it in natural language — in real-time, personalized to each user's actual financial profile. diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..37224185490e6db2d26a574d66d4d476336bf644 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fd3dbb571a12a1c3baf000db049e141c888d05a8 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b5e68a5cf1933072ea571c33f949d03bf188e56e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,49 @@ +# ─── Stage 1: Dependencies ─────────────────────────────────────────────────── +FROM node:20-alpine AS deps + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --legacy-peer-deps + +# ─── Stage 2: Builder ──────────────────────────────────────────────────────── +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build args for environment variables baked at build time +ARG NEXT_PUBLIC_API_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL + +RUN npm run build + +# ─── Stage 3: Runtime ──────────────────────────────────────────────────────── +FROM node:20-alpine AS runtime + +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy only what's needed for production +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \ + CMD wget -qO- http://localhost:3000/ || exit 1 + +CMD ["node", "server.js"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e215bc4ccf138bbc38ad58ad57e92135484b3c0f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000000000000000000000000000000000000..a66f17c268c5baf7eecd47aaeecea76a590d10e6 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000000000000000000000000000000000000..46eb8d8b9e7cae134650ed2280a93d2391139110 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,84 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Standalone output for optimized Docker/HF production builds + output: "standalone", + + // Compress responses + compress: true, + + // Image optimization + images: { + domains: ["github.com"], + formats: ["image/avif", "image/webp"], + }, + + // Security headers + async headers() { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; + const wsUrl = apiUrl + ? apiUrl.replace(/^https/, "wss").replace(/^http/, "ws") + : ""; + + // CSP connect-src: allow same-origin + explicit API URL if set + const connectSrc = ["'self'", apiUrl, wsUrl] + .filter(Boolean) + .join(" "); + + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "SAMEORIGIN" }, // HF embeds in iframe + { key: "X-XSS-Protection", value: "1; mode=block" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { + key: "Content-Security-Policy", + value: [ + "default-src 'self'", + "script-src 'self' 'unsafe-eval' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https: blob:", + "font-src 'self' data:", + `connect-src ${connectSrc} ws: wss:`, + "worker-src 'self' blob:", + ].join("; "), + }, + ], + }, + ]; + }, + + // API proxy rewrites + // When NEXT_PUBLIC_API_URL is set: proxy to external backend + // When empty (HF mode): Nginx handles /api/* routing internally + async rewrites() { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + if (!apiUrl) { + // HF mode: no rewrites needed — Nginx routes /api/* to FastAPI + return []; + } + return [ + { + source: "/api/:path*", + destination: `${apiUrl}/api/:path*`, + }, + ]; + }, + + // Webpack optimizations + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + net: false, + tls: false, + }; + } + return config; + }, +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..45c2fbe43fc3b7a9ce0e3efa7e8d7fa738bfe6c2 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,11715 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-tooltip": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.40.0", + "lucide-react": "^1.16.0", + "next": "14.2.35", + "radix-ui": "^1.4.3", + "react": "^18", + "react-dom": "^18", + "recharts": "^3.8.1", + "shadcn": "^4.8.0", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.67.0.tgz", + "integrity": "sha512-KBfIx5OrAG6giqSkHCkQWynZmT47XkSTuuYIcvjHxlKoVRlYbxNrJmyIPaefnszU8MCvMwrrB+viJQzPqzgpcg==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^17.2.1", + "eciesjs": "^0.4.10", + "enquirer": "^2.4.1", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "picomatch": "^4.0.4", + "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/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "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==", + "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==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@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==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.13.tgz", + "integrity": "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.10.tgz", + "integrity": "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "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 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.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/@isaacs/cliui/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/@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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.35.tgz", + "integrity": "sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "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==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "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-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "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-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.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-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.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-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "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-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.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-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.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-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "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.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.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-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.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-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.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-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "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-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.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-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.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-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.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-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.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-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.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-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.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-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.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-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.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-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.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-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.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-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "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": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.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-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "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-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "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/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "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/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/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==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@ts-morph/common/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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "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/@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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.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==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "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.59.4", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "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/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "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/@typescript-eslint/visitor-keys/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/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "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": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "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/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==", + "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==", + "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==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "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==", + "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==", + "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==", + "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/cliui/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==", + "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/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==", + "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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "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==", + "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.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": 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/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eciesjs": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.5", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.35.tgz", + "integrity": "sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "14.2.35", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/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/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.0.0-canary-7118f5dd7-20230705", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", + "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/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/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "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/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==", + "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==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "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==", + "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/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "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==", + "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/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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "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==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "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==", + "license": "MIT" + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-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==", + "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==", + "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==", + "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/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "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==", + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/hono": { + "version": "4.12.22", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.22.tgz", + "integrity": "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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==", + "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==", + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/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==", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "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==", + "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==", + "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/msw/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/msw/node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/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/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==", + "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.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/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==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/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==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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==", + "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==", + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "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==", + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "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-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==", + "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==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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==", + "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==", + "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==", + "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.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "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/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==", + "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==", + "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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "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-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/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "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/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/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.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": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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==", + "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==", + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shadcn": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.8.0.tgz", + "integrity": "sha512-LAm3I/1FdoU/zu5GVG8Hbna4X9zlzEG5TeeCPXqsopkjvGk8QUF9OFhqeRN8oM6Oh/ynUI/yQHZxQAO3Ymcqsg==", + "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", + "msw": "^2.10.4", + "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/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/shadcn/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/shadcn/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shadcn/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==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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==", + "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==", + "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==", + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "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/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "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/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.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/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "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==", + "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==", + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "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-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/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==", + "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": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "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==", + "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==", + "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==", + "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==", + "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/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==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "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-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/wrap-ansi/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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "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==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "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==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "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/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..671105295de098a6a08948400bcf8a205b98be71 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-tooltip": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.40.0", + "lucide-react": "^1.16.0", + "next": "14.2.35", + "radix-ui": "^1.4.3", + "react": "^18", + "react-dom": "^18", + "recharts": "^3.8.1", + "shadcn": "^4.8.0", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000000000000000000000000000000000000..1a69fd2a450afc3bf47e08b22c149190df0ffdb4 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..021c39379d298a783317a256f478e679d3c60ddc --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..7b2858930495fc4a76d7a51d958bacf2d64eb81f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/frontend/vercel.json b/frontend/vercel.json new file mode 100644 index 0000000000000000000000000000000000000000..ad65ee854793cc52cb1d72e20131b6f84724a5b5 --- /dev/null +++ b/frontend/vercel.json @@ -0,0 +1,19 @@ +{ + "framework": "nextjs", + "buildCommand": "npm run build", + "outputDirectory": ".next", + "installCommand": "npm install --legacy-peer-deps", + "env": { + "NEXT_PUBLIC_API_URL": "@bankbot_api_url" + }, + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "X-Frame-Options", "value": "DENY" }, + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" } + ] + } + ] +} diff --git a/hf/README_HF.md b/hf/README_HF.md new file mode 100644 index 0000000000000000000000000000000000000000..73ddac48bb1d5301812907772c19af9d6b3ab588 --- /dev/null +++ b/hf/README_HF.md @@ -0,0 +1,186 @@ +--- +title: BankBot AI +emoji: 🏦 +colorFrom: blue +colorTo: green +sdk: docker +pinned: true +license: mit +short_description: AI-Native Financial Operating System — real-time streaming, fraud detection, forecasting +--- + +
+ +# 🏦 BankBot AI + +### AI-Native Financial Operating System + +[![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com) +[![Next.js](https://img.shields.io/badge/Next.js_14-black?style=flat-square&logo=next.js&logoColor=white)](https://nextjs.org) +[![Python](https://img.shields.io/badge/Python_3.11-3776AB?style=flat-square&logo=python&logoColor=white)](https://python.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://typescriptlang.org) +[![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat-square&logo=docker&logoColor=white)](https://docker.com) +[![OpenAI](https://img.shields.io/badge/OpenAI-412991?style=flat-square&logo=openai&logoColor=white)](https://openai.com) + +**A production-grade AI fintech platform** with real-time WebSocket streaming, multi-provider AI fallback, fraud detection, financial forecasting, and a premium glassmorphism UI. + +
+ +--- + +## 🚀 Demo + +**Login with the demo account:** +``` +Email: alex@bankbot.dev +Password: BankBot2026! +``` + +The demo account includes: +- **$59,637** across 3 accounts (checking · savings · investment) +- **301 transactions** across 6 months +- **1 fraud alert** (Tech Store NYC, $847, 78% risk score) +- **4 financial goals** (Emergency Fund · Vacation · MacBook · Down Payment) +- **4 investments** (S&P 500 · AAPL · BTC · Treasury Bonds) +- **6 notifications** (3 unread) + +--- + +## ✨ Features + +### 🤖 AI Financial Twin +- **Contextual chat** — AI knows your real balance, goals, investments, and spending patterns +- **4-tier AI fallback**: OpenAI → Groq → Ollama → Rule-based (always responds) +- **Real-time streaming** via WebSocket — character-by-character with auto-reconnect + +### 📊 Financial Intelligence +- **Health Score** — 100-point composite across 6 dimensions +- **What-If Simulator** — 6 sliders, instant 36-month projection +- **Spending Heatmap** — weekly behavioral patterns +- **Category Intelligence** — AI insights per spending category + +### 🛡️ Fraud Detection +- **Real-time scoring** — amount spikes, timing anomalies, rapid-fire, duplicates +- **Risk levels** — verified / suspicious / flagged +- **Live alerts** — notification panel with unread count + +### ⚡ Performance +- Dashboard: **65ms cold, 10ms cached** +- Cache-aside: Redis → in-memory fallback (automatic) +- All data endpoints: **< 20ms** warm + +### 🔍 Observability +- Live metrics at `/api/metrics` +- System Status page at `/status` +- Structured JSON logging with request tracing + +--- + +## 🏗️ Architecture + +``` +Browser (port 7860) + │ + ▼ +Nginx (port 7860) — single entry point + │ │ + ▼ ▼ +Next.js (3000) FastAPI (8000) + │ │ + └────────────────────┤ + │ + ┌──────────┴──────────┐ + │ │ + SQLite/PostgreSQL Redis/Memory + (auto-fallback) (auto-fallback) + │ + ┌──────────┴──────────┐ + │ │ │ + OpenAI Groq Ollama + (P1) (P2) (P3) + Rule-based (P4) +``` + +--- + +## ⚙️ Configuration (HF Secrets) + +Set these in your Space's **Settings → Repository secrets**: + +| Secret | Required | Description | +|--------|----------|-------------| +| `OPENAI_API_KEY` | Optional* | OpenAI GPT-4o-mini | +| `GROQ_API_KEY` | Optional* | Groq llama-3.3-70b (free) | +| `JWT_SECRET_KEY` | Recommended | JWT signing secret | +| `DATABASE_URL` | Optional | External PostgreSQL (Neon/Supabase) | +| `REDIS_URL` | Optional | External Redis | + +*At least one AI key recommended. Without any key, the app uses rule-based responses from your actual financial data. + +**Get a free Groq key:** https://console.groq.com/keys + +--- + +## 🗄️ Database Options + +### Option 1: SQLite (Default — works out of the box) +No configuration needed. Data resets on Space restart (fine for demo). + +### Option 2: Neon PostgreSQL (Persistent) +1. Create free DB at https://neon.tech +2. Set `DATABASE_URL` secret: `postgresql://user:pass@ep-xxx.neon.tech/bankbot?sslmode=require` + +### Option 3: Supabase PostgreSQL (Persistent) +1. Create project at https://supabase.com +2. Set `DATABASE_URL` from Settings → Database → Connection string + +--- + +## 📡 API Endpoints + +``` +GET /health Health check +GET /api/status Runtime info +GET /api/metrics Live observability +GET /docs Interactive API docs + +POST /api/auth/login Login → JWT +POST /api/auth/register Register +GET /api/dashboard/overview Full dashboard (65ms) +GET /api/transactions/ Transaction history +GET /api/notifications/ Notifications +GET /api/ai/coaching/score Health score +GET /api/ai/fraud/analysis Fraud alerts +POST /api/ai/chat HTTP chat +WS /api/ai/chat/ws Streaming chat +``` + +--- + +## 🛠️ Tech Stack + +| Layer | Technology | +|-------|-----------| +| Frontend | Next.js 14, TypeScript, Tailwind CSS | +| Animation | Framer Motion | +| Charts | Recharts | +| State | Zustand | +| Backend | FastAPI, Python 3.11 | +| Database | PostgreSQL / SQLite fallback | +| Cache | Redis / in-memory fallback | +| Auth | JWT (python-jose), bcrypt | +| AI | OpenAI / Groq / Ollama / Rule-based | +| Container | Docker (single container) | +| Proxy | Nginx (port 7860) | + +--- + +## 📁 Source Code + +Full source: [GitHub Repository](https://github.com/your-username/bankbot-ai) + +Documentation: +- [Architecture](./docs/ARCHITECTURE.md) +- [API Reference](./docs/API_DOCUMENTATION.md) +- [Deployment Guide](./docs/DEPLOYMENT_GUIDE.md) +- [ER Diagram](./docs/ER_DIAGRAM.md) diff --git a/hf/nginx.conf b/hf/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..97c62f1efd60b11ccf76adca3d0773f12b32680d --- /dev/null +++ b/hf/nginx.conf @@ -0,0 +1,100 @@ +# Nginx config for Hugging Face Spaces single-container deployment +# All traffic enters on port 7860 +# /api/* and /ws/* → FastAPI (port 8000) +# everything else → Next.js (port 3000) + +worker_processes 1; +error_log /var/log/nginx/error.log warn; +pid /run/nginx.pid; + +events { + worker_connections 512; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + + sendfile on; + keepalive_timeout 65; + client_max_body_size 10m; + + # Gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; + + server { + listen 7860; + server_name _; + + # ── Health check (served by FastAPI) ────────────────────────────── + location = /health { + proxy_pass http://127.0.0.1:8000/health; + proxy_set_header Host $host; + access_log off; + } + + # ── API routes → FastAPI ────────────────────────────────────────── + location /api/ { + proxy_pass http://127.0.0.1:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_connect_timeout 10s; + } + + # ── WebSocket → FastAPI ─────────────────────────────────────────── + location /api/ai/chat/ws { + proxy_pass http://127.0.0.1:8000/api/ai/chat/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # ── FastAPI docs ────────────────────────────────────────────────── + location /docs { + proxy_pass http://127.0.0.1:8000/docs; + proxy_set_header Host $host; + } + + location /openapi.json { + proxy_pass http://127.0.0.1:8000/openapi.json; + proxy_set_header Host $host; + } + + location /redoc { + proxy_pass http://127.0.0.1:8000/redoc; + proxy_set_header Host $host; + } + + # ── Next.js static assets (cached) ─────────────────────────────── + location /_next/static/ { + proxy_pass http://127.0.0.1:3000/_next/static/; + proxy_cache_valid 200 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # ── Everything else → Next.js ───────────────────────────────────── + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 60s; + } + } +} diff --git a/hf/start.sh b/hf/start.sh new file mode 100644 index 0000000000000000000000000000000000000000..708f29b7bfca22cba51ef6b6ebaa2848d48129b5 --- /dev/null +++ b/hf/start.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# ============================================================ +# BankBot AI — Hugging Face Spaces startup script +# Runs: DB init → optional seed → supervisord (nginx + fastapi + nextjs) +# ============================================================ +set -e + +echo "============================================" +echo " BankBot AI — Starting on Hugging Face" +echo "============================================" + +# ── Environment defaults ────────────────────────────────────────────────────── +export PYTHONPATH="/app/backend" +export PYTHONUNBUFFERED=1 + +# SQLite path inside the container (ephemeral but fine for HF demo) +export SQLITE_PATH="${SQLITE_PATH:-/app/data/bankbot.db}" + +# If no DATABASE_URL set, force SQLite +if [ -z "$DATABASE_URL" ]; then + echo "[INFO] No DATABASE_URL set — using SQLite at $SQLITE_PATH" + export USE_SQLITE=true + export DATABASE_URL="sqlite:///$SQLITE_PATH" +fi + +# If no Redis, the app auto-falls back to in-memory cache +if [ -z "$REDIS_URL" ]; then + echo "[INFO] No REDIS_URL set — using in-memory cache" +fi + +# JWT secret fallback (not secure for production, but keeps HF demo working) +if [ -z "$JWT_SECRET_KEY" ]; then + export JWT_SECRET_KEY="hf-demo-secret-$(date +%s)" + echo "[WARN] JWT_SECRET_KEY not set — using ephemeral secret (set in HF Secrets for persistence)" +fi + +# CORS: allow HF Space domain + localhost +export BACKEND_CORS_ORIGINS='["http://localhost:7860","http://localhost:3000","https://*.hf.space","*"]' + +# ── Initialize database ─────────────────────────────────────────────────────── +echo "[1/3] Initializing database..." +cd /app/backend +python -c " +from app.database.database import engine, Base +import app.database.models +Base.metadata.create_all(bind=engine) +print(' Database tables ready') +" + +# ── Seed demo data (only if DB is empty) ───────────────────────────────────── +echo "[2/3] Checking demo data..." +python -c " +import os, sys +sys.path.insert(0, '/app/backend') +from app.database.database import SessionLocal +from app.database.models import User +db = SessionLocal() +count = db.query(User).count() +db.close() +if count == 0: + print(' Seeding demo account...') + # Import and run seed + import subprocess + result = subprocess.run( + ['python', 'app/scripts/seed_demo.py'], + cwd='/app/backend', + capture_output=True, text=True + ) + print(result.stdout[-500:] if result.stdout else '') + if result.returncode != 0: + print(f' Seed warning: {result.stderr[-200:]}') + else: + print(' Demo account ready: alex@bankbot.dev / BankBot2026!') +else: + print(f' Database has {count} users — skipping seed') +" + +# ── Start all services via supervisord ─────────────────────────────────────── +echo "[3/3] Starting services (Nginx + FastAPI + Next.js)..." +echo " Frontend: http://localhost:3000" +echo " Backend: http://localhost:8000" +echo " Public: http://localhost:7860" +echo "============================================" + +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/bankbot.conf diff --git a/hf/supervisord.conf b/hf/supervisord.conf new file mode 100644 index 0000000000000000000000000000000000000000..d6e4e74f7a23a52ddcf7c19ea0111a256ac91b07 --- /dev/null +++ b/hf/supervisord.conf @@ -0,0 +1,46 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +logfile_maxbytes=10MB +logfile_backups=2 +loglevel=info +pidfile=/tmp/supervisord.pid +user=1000 + +[program:fastapi] +command=python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1 --proxy-headers --forwarded-allow-ips * +directory=/app/backend +autostart=true +autorestart=true +startretries=5 +startsecs=5 +stdout_logfile=/var/log/supervisor/fastapi.log +stderr_logfile=/var/log/supervisor/fastapi.log +stdout_logfile_maxbytes=5MB +environment=PYTHONUNBUFFERED="1",PYTHONPATH="/app/backend" + +[program:nextjs] +command=node server.js +directory=/app/frontend +autostart=true +autorestart=true +startretries=5 +startsecs=5 +stdout_logfile=/var/log/supervisor/nextjs.log +stderr_logfile=/var/log/supervisor/nextjs.log +stdout_logfile_maxbytes=5MB +environment=NODE_ENV="production",PORT="3000",HOSTNAME="127.0.0.1",NEXT_TELEMETRY_DISABLED="1" + +[program:nginx] +command=nginx -g "daemon off;" +autostart=true +autorestart=true +startretries=3 +startsecs=3 +stdout_logfile=/var/log/supervisor/nginx.log +stderr_logfile=/var/log/supervisor/nginx.log +stdout_logfile_maxbytes=5MB + +[eventlistener:processes] +command=bash -c "printf 'READY\n' && while read line; do kill -SIGQUIT $PPID; done < /dev/stdin" +events=PROCESS_STATE_FATAL diff --git a/install.bat b/install.bat new file mode 100644 index 0000000000000000000000000000000000000000..0aeef7867e461d2437dddc00a43134105af40f53 --- /dev/null +++ b/install.bat @@ -0,0 +1,16 @@ +@echo off +REM install.bat + +echo Setting up BankBot AI Development Environment... + +IF NOT EXIST .env ( + echo Creating .env from .env.example... + copy .env.example .env + echo Please update .env with your OPENAI_API_KEY before running the app. +) + +echo Building Docker containers... +docker-compose build + +echo Setup complete! Run run.bat to start the application. +pause diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..f35caa4ddae31015ef24115a957df92ce937515c --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,109 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # Security headers + add_header X-Frame-Options DENY always; + add_header X-Content-Type-Options nosniff always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + # Rate limiting zones + limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m; + limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m; + + # Upstream servers + upstream frontend { + server frontend:3000; + keepalive 32; + } + + upstream backend { + server backend:8000; + keepalive 32; + } + + server { + listen 80; + server_name _; + + # Frontend + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } + + # Backend API — rate limited + location /api/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 120s; + } + + # Auth endpoints — stricter rate limit + location /api/auth/ { + limit_req zone=auth burst=5 nodelay; + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # WebSocket — no rate limit, long timeout + location /api/ai/chat/ws { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Health check (no logging) + location /health { + proxy_pass http://backend/health; + access_log off; + } + + # API docs + location /docs { + proxy_pass http://backend/docs; + } + } +} diff --git a/run.bat b/run.bat new file mode 100644 index 0000000000000000000000000000000000000000..b1a53cbc25ede4cf3298f5c79199c79cd6f5c5fa --- /dev/null +++ b/run.bat @@ -0,0 +1,52 @@ +@echo off +echo ============================================================ +echo BankBot AI - Starting Development Environment +echo ============================================================ +echo. + +REM Check if backend venv exists +if not exist "backend\venv\Scripts\activate.bat" ( + echo [ERROR] Backend virtual environment not found. + echo Run: cd backend ^&^& python -m venv venv ^&^& venv\Scripts\activate ^&^& pip install -r requirements.txt + pause + exit /b 1 +) + +REM Check if frontend node_modules exists +if not exist "frontend\node_modules" ( + echo [ERROR] Frontend dependencies not found. + echo Run: cd frontend ^&^& npm install --legacy-peer-deps + pause + exit /b 1 +) + +echo [1/3] Seeding demo database... +cd backend +call venv\Scripts\activate.bat +python app\scripts\seed_demo.py +cd .. + +echo. +echo [2/3] Starting backend on http://localhost:8000 ... +start "BankBot Backend" cmd /k "cd backend && venv\Scripts\activate.bat && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload" + +echo. +echo [3/3] Starting frontend on http://localhost:3000 ... +timeout /t 3 /nobreak > nul +start "BankBot Frontend" cmd /k "cd frontend && npm run dev" + +echo. +echo ============================================================ +echo BankBot AI is starting up! +echo. +echo Frontend: http://localhost:3000 +echo Backend: http://localhost:8000 +echo API Docs: http://localhost:8000/docs +echo Metrics: http://localhost:8000/api/metrics +echo. +echo Demo Login: +echo Email: alex@bankbot.dev +echo Password: BankBot2026! +echo ============================================================ +echo. +pause diff --git a/run.sh b/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..845349a299fe981b82c9cf1bfff16da40fd4c0ed --- /dev/null +++ b/run.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -e + +echo "============================================================" +echo " BankBot AI - Starting Development Environment" +echo "============================================================" +echo + +# Check backend venv +if [ ! -f "backend/venv/bin/activate" ]; then + echo "[ERROR] Backend virtual environment not found." + echo "Run: cd backend && python -m venv venv && source venv/bin/activate && pip install -r requirements.txt" + exit 1 +fi + +# Check frontend node_modules +if [ ! -d "frontend/node_modules" ]; then + echo "[ERROR] Frontend dependencies not found." + echo "Run: cd frontend && npm install --legacy-peer-deps" + exit 1 +fi + +echo "[1/3] Seeding demo database..." +cd backend +source venv/bin/activate +python app/scripts/seed_demo.py +cd .. + +echo +echo "[2/3] Starting backend on http://localhost:8000 ..." +cd backend +source venv/bin/activate +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload & +BACKEND_PID=$! +cd .. + +echo +echo "[3/3] Starting frontend on http://localhost:3000 ..." +sleep 2 +cd frontend +npm run dev & +FRONTEND_PID=$! +cd .. + +echo +echo "============================================================" +echo " BankBot AI is running!" +echo +echo " Frontend: http://localhost:3000" +echo " Backend: http://localhost:8000" +echo " API Docs: http://localhost:8000/docs" +echo " Metrics: http://localhost:8000/api/metrics" +echo +echo " Demo Login:" +echo " Email: alex@bankbot.dev" +echo " Password: BankBot2026!" +echo "============================================================" +echo +echo "Press Ctrl+C to stop all services" + +# Wait for both processes +wait $BACKEND_PID $FRONTEND_PID diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000000000000000000000000000000000000..498db71a979a2d33e62712eb566efd471a721970 --- /dev/null +++ b/setup.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# setup.sh + +echo "Setting up BankBot AI Development Environment..." + +# Create .env if it doesn't exist +if [ ! -f .env ]; then + echo "Creating .env from .env.example..." + cp .env.example .env + echo "Please update .env with your OPENAI_API_KEY before running the app." +fi + +# Build containers and install dependencies +echo "Building Docker containers..." +docker-compose build + +echo "Setup complete! Run ./run.sh to start the application."