diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 4c662c0..8f51a6b 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -19,20 +19,26 @@ jobs: with: python-version: '3.11' - - name: Install Performance Harness Dependencies - run: | - if [ -f "requirements.txt" ]; then pip install -r requirements.txt; fi - pip install pytest pytest-benchmark pytest-asyncio websockets cryptography fastapi + - name: Install benchmark dependencies + run: pip install -r requirements-dev.txt - name: Execute Profiling Suite & Track Latency Metrics env: PYTHONPATH: . run: | - echo "Executing high-concurrency message broadcast and cryptographic routine benchmarks..." - # Runs benchmarks if present, otherwise outputs structured system performance logging - if [ -d "tests/benchmarks" ]; then - pytest tests/benchmarks/ --benchmark-json=output.json - else - echo "Executing isolated performance profile for local encryption times and pub/sub message dispatch speeds..." - echo "Performance metrics captured successfully. Broadcast pipelines remain sub-millisecond." - fi + python -m benchmarks.run_benchmark --output benchmarks/latest.json + python -m pytest tests/test_benchmark.py -q + python - <<'PY' + import json + result = json.load(open("benchmarks/latest.json", encoding="utf-8")) + regression = result["comparison"]["throughput_change_percent"] + if regression < -15: + raise SystemExit(f"Throughput regression {regression}% exceeds 15% budget") + PY + - name: Upload benchmark evidence + uses: actions/upload-artifact@v4 + with: + name: benchmark-report + path: | + benchmarks/latest.json + benchmarks/benchmark_report.md diff --git a/.github/workflows/security-supply-chain.yml b/.github/workflows/security-supply-chain.yml index 08c337a..8a48750 100644 --- a/.github/workflows/security-supply-chain.yml +++ b/.github/workflows/security-supply-chain.yml @@ -54,8 +54,9 @@ jobs: format: sarif output: trivy-results.sarif severity: HIGH,CRITICAL + limit-severities-for-sarif: true ignore-unfixed: true - exit-code: "0" + exit-code: "1" - name: Upload SARIF uses: github/codeql-action/upload-sarif@v4 with: @@ -88,4 +89,4 @@ jobs: format: table severity: HIGH,CRITICAL ignore-unfixed: true - exit-code: "0" + exit-code: "1" diff --git a/.github/workflows/trojanchat-hygiene.yml b/.github/workflows/trojanchat-hygiene.yml index 4c65498..1665eea 100644 --- a/.github/workflows/trojanchat-hygiene.yml +++ b/.github/workflows/trojanchat-hygiene.yml @@ -26,11 +26,19 @@ jobs: python-version: "3.11" cache: pip - name: Install quality tooling - run: python -m pip install --upgrade pip ruff black + run: python -m pip install --upgrade pip ruff black bandit - name: Validate Python syntax run: python -m compileall -q app backend tests - - name: Run blocking Ruff correctness checks - run: ruff check app backend tests/test_api.py tests/test_chat_service.py tests/test_inference_cache.py --select E9,F63,F7,F82 + - name: Run blocking Ruff and Bandit checks + run: | + ruff check backend/api.py backend/routes backend/services app/core/security.py benchmarks tests/test_api.py tests/test_backend_hardening.py tests/test_benchmark.py tests/test_security.py + bandit -q backend/api.py backend/config.py backend/routes/*.py backend/services/*.py app/core/inference_cache.py app/core/security.py app/services/chat_service.py -f json -o bandit-report.json + - name: Upload static-analysis report + if: always() + uses: actions/upload-artifact@v4 + with: + name: static-analysis-report + path: bandit-report.json - name: Publish formatting advisory run: | black --check app backend tests/test_api.py tests/test_chat_service.py tests/test_inference_cache.py \ @@ -63,7 +71,16 @@ jobs: tests/test_api.py \ tests/test_chat_service.py \ tests/test_inference_cache.py \ - --cov=app \ + tests/test_backend_hardening.py \ + tests/test_benchmark.py \ + tests/test_security.py \ + --cov=backend.api \ + --cov=backend.routes.chat_routes \ + --cov=backend.routes.ws_routes \ + --cov=backend.services.chat_service \ + --cov=app.core.inference_cache \ + --cov=app.services.chat_service \ + --cov-fail-under=90 \ --cov-report=term-missing \ --cov-report=xml \ --junitxml=pytest-results.xml \ @@ -90,13 +107,9 @@ jobs: cache: pip - name: Install audit tooling run: pip install pip-audit - - name: Generate dependency audit report + - name: Enforce dependency audit run: | - set +e pip-audit -r requirements.txt --progress-spinner off --format json --output pip-audit.json - status=$? - echo "pip-audit completed with status ${status}; findings are retained as an artifact for triage." - exit 0 - name: Upload dependency audit uses: actions/upload-artifact@v4 with: @@ -104,6 +117,26 @@ jobs: path: pip-audit.json if-no-files-found: warn + frontend-build: + name: "Continuous Integration / Frontend Build" + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + - name: Install locked dependencies + run: npm ci --ignore-scripts + - name: Enforce high-severity dependency audit + run: npm audit --audit-level=high + - name: Build production frontend + run: npm run build + container-smoke-test: name: "Deployment / Container Build & Smoke Test" needs: test-matrix @@ -126,7 +159,7 @@ jobs: release-readiness: name: "Release Engineering / Artifact Readiness" - needs: [test-matrix, dependency-audit, container-smoke-test] + needs: [test-matrix, dependency-audit, frontend-build, container-smoke-test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 6129322..770812c 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,15 +1,33 @@ # Changelog All notable changes to TrojanChat will be documented in this file. +## [Unreleased] +### Added +- Reproducible before/after latency, throughput, and peak-memory benchmark with raw JSON evidence. +- Unit and integration tests for bounded storage, negative API cases, and WebSocket lifecycle. +- CodeQL default-setup documentation, blocking static/security scans, audit, and production checklist. + +### Changed +- Canonical critical-path coverage is enforced at 90%. +- Message retention is bounded and thread-safe; API inputs and history queries are constrained. + +### Performance +- Reference run reduced peak Python allocations by 80.06% with a 6.8% throughput tradeoff. + +### Security +- Sanitized internal errors and changed dependency/Trivy findings from advisory to blocking. +- Upgraded Next.js from 14.2.15 to 15.5.20 to remove current HIGH-severity advisories; added a blocking frontend audit and production build. +- Removed pip/setuptools/wheel build tooling from the runtime image and constrained SARIF output to the declared HIGH/CRITICAL policy. + ## [0.2.0] - 2026-04-02 ### Changed - **Python runtime**: Docker base image upgraded from `python:3.10-slim` to `python:3.12-slim` -- **CI/CD**: GitHub Actions updated — `actions/checkout@v3→v4`, `actions/setup-python@v4→v5`, Python `3.11→3.12` +- **CI/CD**: GitHub Actions updated — `actions/checkout@v3→v4`, `actions/setup-python@v4→v5`, Python `3.11→3.12` - **CI/CD**: Fixed `backend/requirements.txt` reference (created missing file) and corrected test runner path - **requirements.txt**: Pinned minimum versions for all Python dependencies; added `pydantic-settings>=2.5.0`, `fastapi>=0.115.0`, `uvicorn>=0.30.0`, `httpx>=0.27.0`, `pytest-asyncio>=0.24.0` - **Pydantic v2**: Migrated `BaseSettings` import in `app/core/app/core/config.py` from `pydantic` to `pydantic_settings` package - **Deprecation fixes**: Replaced `datetime.utcnow()` (deprecated in Python 3.12) with `datetime.now(timezone.utc)` in `mcp_adapter.py`, `app/core/security.py`, and `backend/services/chat_service.py` -- **Node.js (server/)**: Upgraded dependencies — `express@4.18→4.21`, `socket.io@4.7→4.8`, `mongoose@7→8`, `dotenv@16.0→16.4`, `jest@29.5→29.7`, `supertest@6→7`, `nodemon@2→3` +- **Node.js (server/)**: Upgraded dependencies — `express@4.18→4.21`, `socket.io@4.7→4.8`, `mongoose@7→8`, `dotenv@16.0→16.4`, `jest@29.5→29.7`, `supertest@6→7`, `nodemon@2→3` ### Breaking Changes - **Pydantic v2**: `BaseSettings` is no longer exported from `pydantic`; it requires the `pydantic-settings` package. Run `pip install pydantic-settings` or use the updated `requirements.txt`. @@ -31,4 +49,3 @@ All notable changes to TrojanChat will be documented in this file. ### Security - Added JWT expiration - Added request rate limiting - diff --git a/Dockerfile b/Dockerfile index cdc33a7..d9fd4a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,9 @@ WORKDIR /build COPY requirements.txt . RUN python -m venv /opt/venv \ && /opt/venv/bin/pip install --upgrade pip \ - && /opt/venv/bin/pip install -r requirements.txt + && /opt/venv/bin/pip install -r requirements.txt \ + && /opt/venv/bin/pip uninstall --yes setuptools wheel \ + && /opt/venv/bin/pip uninstall --yes pip FROM python:3.11-slim AS runtime @@ -16,7 +18,11 @@ ENV PATH=/opt/venv/bin:$PATH \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONPATH=/app -RUN useradd --create-home --uid 10001 appuser +# The base image ships build tooling that is unnecessary at runtime. Removing it +# also prevents scanners from treating setuptools' vendored build dependencies +# as remotely exploitable application packages. +RUN python -m pip uninstall --yes setuptools wheel \ + && useradd --create-home --uid 10001 appuser WORKDIR /app COPY --from=builder /opt/venv /opt/venv diff --git a/README.md b/README.md index 37fe522..0c17a58 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ # TrojanChat +[![CI](https://github.com/CoreyLeath-code/TrojanChat/actions/workflows/trojanchat-hygiene.yml/badge.svg)](https://github.com/CoreyLeath-code/TrojanChat/actions/workflows/trojanchat-hygiene.yml) +[![Benchmarks](https://github.com/CoreyLeath-code/TrojanChat/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/CoreyLeath-code/TrojanChat/actions/workflows/benchmarks.yml) +[![Security](https://github.com/CoreyLeath-code/TrojanChat/actions/workflows/security-supply-chain.yml/badge.svg)](https://github.com/CoreyLeath-code/TrojanChat/actions/workflows/security-supply-chain.yml) +[![CodeQL](https://img.shields.io/badge/CodeQL-default%20setup-enabled-2F80ED?logo=github)](https://github.com/CoreyLeath-code/TrojanChat/security/code-scanning) +[![Release](https://img.shields.io/github/v/release/CoreyLeath-code/TrojanChat?display_name=tag)](https://github.com/CoreyLeath-code/TrojanChat/releases) +[![Coverage](https://img.shields.io/badge/critical--path%20coverage-92.57%25-success)](#engineering-evidence) +[![Throughput](https://img.shields.io/badge/throughput-40%2C326.50%20msg%2Fs-blue)](#research-benchmark) +[![Peak memory](https://img.shields.io/badge/peak%20memory-4.228%20MiB-blueviolet)](#research-benchmark) +[![Memory improvement](https://img.shields.io/badge/memory%20reduction-80.06%25-success)](#research-benchmark) +[![Benchmark date](https://img.shields.io/badge/benchmark-2026--07--18-informational)](benchmarks/benchmark_report.md) ![Python](https://img.shields.io/badge/Python-3.11%20%7C%203.12-3776AB?logo=python&logoColor=white) ![FastAPI](https://img.shields.io/badge/FastAPI-Backend-009688?logo=fastapi&logoColor=white) ![Streamlit](https://img.shields.io/badge/Streamlit-Frontend-FF4B4B?logo=streamlit&logoColor=white) @@ -30,6 +40,49 @@ TrojanChat is a production-hardened, multi-client chat architecture optimized for high-concurrency environments. Moving away from standard blocking network sockets, this platform leverages asynchronous event loops to maintain thousands of concurrent connections efficiently while maintaining structural memory efficiency. +## Engineering evidence + +| Evidence | Current result | Enforcement | +|---|---:|---| +| Unit + integration tests | 30 passing locally | Python 3.11/3.12 CI matrix | +| Critical-path coverage | 92.57% | CI fails below 90% | +| Median / P99 write latency | 1,239.880 / 1,303.501 ms per 50k-message run | Reproducible benchmark artifact | +| Throughput | 40,326.50 messages/s | Regression budget: no worse than -15% | +| Peak Python memory | 4.228 MiB | 80.06% below legacy baseline | +| Static analysis | Ruff + Bandit | Blocking; JSON report retained | +| Security | CodeQL, Gitleaks, pip-audit, Trivy | Blocking on secrets and actionable vulnerabilities | +| Supply chain | CycloneDX SBOM + Dependabot | Artifact per run; weekly updates | + +The reference benchmark uses Windows 11, Python 3.12.13, 50,000 messages, seven iterations, +and a 10,000-message retention bound. Results describe this microbenchmark—not end-to-end network +latency or a production SLO. See the [benchmark methodology](benchmarks/benchmark_report.md), +[audit](docs/AUDIT.md), [architecture](ARCHITECTURE.md), [deployment guide](DEPLOYMENT.MD), and +[production checklist](docs/PRODUCTION_CHECKLIST.md). + +## Research benchmark + +**Question.** Can bounded, synchronized retention stop unbounded memory growth without exceeding a +15% write-throughput regression budget? + +| Metric | Legacy list | Bounded, synchronized store | Relative change | +|---|---:|---:|---:| +| Mean latency / 50k writes | 1,141.831 ms | 1,237.607 ms | +8.4% | +| Median latency / 50k writes | 1,155.519 ms | 1,239.880 ms | +7.3% | +| P95 / P99 latency | 1,221.577 ms | 1,303.501 ms | +6.7% | +| Minimum / maximum latency | 1,063.323 / 1,221.577 ms | 1,180.249 / 1,303.501 ms | observed range | +| Throughput | 43,270.60 msg/s | 40,326.50 msg/s | **−6.8%** | +| Peak Python allocations | 21.205 MiB | 4.228 MiB | **−80.06%** | + +**Method.** Seven independent iterations insert 50,000 structurally identical messages. Both +variants generate UUID4 identifiers and UTC timestamps; only storage and synchronization differ. +Latency uses `time.perf_counter`, memory uses `tracemalloc`, and throughput is derived from median +elapsed time. The raw, versioned result is [`benchmarks/latest.json`](benchmarks/latest.json). + +**Interpretation.** The optimized store remains inside the pre-declared 15% throughput budget while +substantially reducing peak Python allocations. The experiment does not measure network transport, +JSON serialization, Redis, database persistence, multi-process contention, CPU utilization, or RSS. +CI reruns the benchmark on Ubuntu/Python 3.11 and uploads the raw result for per-commit comparison. + --- Architectural Overview @@ -45,7 +98,7 @@ The platform splits operations across an event-driven system architecture to eli --- -## 🚀 Getting Started +## 🚀 Getting Started ### Prerequisites * Python 3.11 or higher @@ -74,20 +127,20 @@ The platform splits operations across an event-driven system architecture to eli python client.py "Corey" ``` -├── chat_core/ -│ ├── __init__.py -│ ├── config.py # <-- Application security settings & validation rules -│ ├── crypto_broker.py # <-- Challenge generation and key distribution logic -│ └── connection_manager.py # <-- Tier 5: High-concurrency socket tracking state loop -├── deployment/ -│ ├── gateway.conf # <-- Tier 2: Envoy or Nginx reverse-proxy ingress rules -│ ├── docker-compose.yml # <-- Full local container environment mesh (Redis, App, Postgres) -│ └── Dockerfile # <-- Minimal production runtime workspace blueprint -├── tests/ -│ ├── unit/ # <-- Job #3: Asynchronous socket validation benches -│ └── schemas/ # <-- Job #7: JSON framing contract tests -├── dailylog.md # <-- Maintenance operations history ledger -└── requirements.txt # <-- Managed dependencies +├── chat_core/ +│ ├── __init__.py +│ ├── config.py # <-- Application security settings & validation rules +│ ├── crypto_broker.py # <-- Challenge generation and key distribution logic +│ └── connection_manager.py # <-- Tier 5: High-concurrency socket tracking state loop +├── deployment/ +│ ├── gateway.conf # <-- Tier 2: Envoy or Nginx reverse-proxy ingress rules +│ ├── docker-compose.yml # <-- Full local container environment mesh (Redis, App, Postgres) +│ └── Dockerfile # <-- Minimal production runtime workspace blueprint +├── tests/ +│ ├── unit/ # <-- Job #3: Asynchronous socket validation benches +│ └── schemas/ # <-- Job #7: JSON framing contract tests +├── dailylog.md # <-- Maintenance operations history ledger +└── requirements.txt # <-- Managed dependencies Engineering Roadmap diff --git a/app/core/security.py b/app/core/security.py index 36da76e..ea60b9f 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -2,9 +2,9 @@ import os from datetime import datetime, timedelta, timezone -from typing import Optional -from jose import JWTError, jwt +import jwt +from jwt import InvalidTokenError from passlib.context import CryptContext SECRET_KEY = os.getenv("SECRET_KEY") @@ -27,7 +27,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): +def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() expire = datetime.now(timezone.utc) + ( @@ -42,5 +42,5 @@ def verify_token(token: str): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload - except JWTError: - return None \ No newline at end of file + except InvalidTokenError: + return None diff --git a/backend/api.py b/backend/api.py index cb83697..fa6180c 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,6 +1,9 @@ from fastapi import FastAPI + from backend.config import settings from backend.routes.chat_routes import router as chat_router +from backend.routes.ws_routes import router as websocket_router + def create_app() -> FastAPI: """ @@ -15,6 +18,7 @@ def create_app() -> FastAPI: # Include application routes app.include_router(chat_router, prefix="/chat", tags=["Chat"]) + app.include_router(websocket_router, tags=["WebSocket"]) @app.get("/", tags=["Health"]) async def root(): diff --git a/backend/config.py b/backend/config.py index 7668d4c..699df4c 100644 --- a/backend/config.py +++ b/backend/config.py @@ -18,7 +18,7 @@ class Settings: API_VERSION: str = "v1" # Local development settings - HOST: str = os.getenv("HOST", "0.0.0.0") + HOST: str = os.getenv("HOST", "127.0.0.1") PORT: int = int(os.getenv("PORT", "10000")) settings = Settings() diff --git a/backend/routes/chat_routes.py b/backend/routes/chat_routes.py index 0488230..51aea72 100644 --- a/backend/routes/chat_routes.py +++ b/backend/routes/chat_routes.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field + from backend.services.chat_service import ChatService router = APIRouter() @@ -11,8 +12,9 @@ # ------------------------------------------- class MessageRequest(BaseModel): - username: str - content: str + model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) + username: str = Field(min_length=1, max_length=32, pattern=r"^[\w .'-]+$") + content: str = Field(min_length=1, max_length=2_000) class MessageResponse(BaseModel): @@ -37,8 +39,8 @@ async def send_message(message: MessageRequest): content=message.content ) return new_message - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + except Exception as exc: + raise HTTPException(status_code=503, detail="Message store unavailable") from exc @router.get("/history") @@ -47,7 +49,11 @@ async def get_message_history(limit: int = 50): Returns the latest N chat messages. """ try: + if not 1 <= limit <= 200: + raise HTTPException(status_code=422, detail="limit must be between 1 and 200") messages = chat_service.get_messages(limit=limit) return messages - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=503, detail="Message store unavailable") from exc diff --git a/backend/services/chat_service.py b/backend/services/chat_service.py index da7042c..f9be32d 100644 --- a/backend/services/chat_service.py +++ b/backend/services/chat_service.py @@ -1,7 +1,9 @@ -import uuid import datetime +import os +import threading +import uuid +from collections import deque from datetime import timezone -from backend.config import settings class ChatService: @@ -11,10 +13,16 @@ class ChatService: to be upgraded easily to Firebase, Redis, or SQL. """ - def __init__(self): - # Temporary in-memory message store - # Structure: { "id": str, "username": str, "content": str, "timestamp": str } - self.messages = [] + def __init__(self, max_messages: int | None = None): + """Create a bounded, thread-safe store for one process. + + A durable shared store is still required for multi-instance production deployments. + """ + configured_limit = max_messages or int(os.getenv("CHAT_HISTORY_LIMIT", "10000")) + if configured_limit < 1: + raise ValueError("max_messages must be positive") + self.messages = deque(maxlen=configured_limit) + self._lock = threading.RLock() def _generate_message_id(self): """ @@ -44,7 +52,8 @@ def send_message(self, username: str, content: str): } # In-memory storage (can replace with Firebase/Postgres/etc.) - self.messages.append(message) + with self._lock: + self.messages.append(message) return message @@ -52,4 +61,7 @@ def get_messages(self, limit: int = 50): """ Returns the latest 'limit' chat messages. """ - return self.messages[-limit:] + if limit < 1: + return [] + with self._lock: + return list(self.messages)[-limit:] diff --git a/benchmarks/benchmark_report.md b/benchmarks/benchmark_report.md new file mode 100644 index 0000000..c49d4d2 --- /dev/null +++ b/benchmarks/benchmark_report.md @@ -0,0 +1,28 @@ +# Benchmark report + +## Research question + +Can bounded, synchronized in-process retention prevent unbounded memory growth without exceeding a +15% write-throughput regression budget relative to the legacy list store? + +## Protocol + +Seven independent iterations insert 50,000 messages. Both variants perform identical UUID4 and UTC +timestamp generation. The optimized store retains the latest 10,000 messages. `perf_counter` +measures elapsed time and `tracemalloc` measures Python peak allocations. Results below are from +Windows 11 and Python 3.12.13 on 2026-07-18; CI regenerates JSON on Ubuntu/Python 3.11. + +| Metric | Legacy baseline | Bounded store | Change | +|---|---:|---:|---:| +| Median latency | 1,155.519 ms | 1,239.880 ms | +7.3% | +| P95 / P99 latency | 1,221.577 ms | 1,303.501 ms | +6.7% | +| Throughput | 43,270.60 msg/s | 40,326.50 msg/s | -6.8% | +| Peak Python memory | 21.205 MiB | 4.228 MiB | **-80.06%** | + +The 6.8% throughput tradeoff is within the 15% budget. This microbenchmark excludes network I/O, +JSON encoding, Redis, persistence, multi-process contention, CPU utilization, and allocator RSS. +It is systems evidence, not a production SLO. Raw evidence is in `benchmarks/latest.json`. + +```bash +python -m benchmarks.run_benchmark --samples 50000 --iterations 7 --retention 10000 +``` diff --git a/benchmarks/latest.json b/benchmarks/latest.json new file mode 100644 index 0000000..3adea42 --- /dev/null +++ b/benchmarks/latest.json @@ -0,0 +1,37 @@ +{ + "schema_version": "1.0.0", + "generated_at_utc": "2026-07-18T19:23:33.494656+00:00", + "parameters": { + "samples": 50000, + "iterations": 7, + "retention": 10000 + }, + "environment": { + "python": "3.12.13", + "platform": "Windows-11-10.0.26200-SP0" + }, + "baseline": { + "latency_mean_ms": 1141.831, + "latency_median_ms": 1155.519, + "latency_p95_ms": 1221.577, + "latency_p99_ms": 1221.577, + "latency_min_ms": 1063.323, + "latency_max_ms": 1221.577, + "throughput_messages_per_second": 43270.6, + "peak_memory_mib": 21.205 + }, + "optimized": { + "latency_mean_ms": 1237.607, + "latency_median_ms": 1239.88, + "latency_p95_ms": 1303.501, + "latency_p99_ms": 1303.501, + "latency_min_ms": 1180.249, + "latency_max_ms": 1303.501, + "throughput_messages_per_second": 40326.5, + "peak_memory_mib": 4.228 + }, + "comparison": { + "throughput_change_percent": -6.8, + "peak_memory_change_percent": -80.06 + } +} diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py new file mode 100644 index 0000000..8c02e69 --- /dev/null +++ b/benchmarks/run_benchmark.py @@ -0,0 +1,101 @@ +"""Reproducible before/after benchmark for the canonical message store.""" + +import argparse +import json +import platform +import statistics +import time +import tracemalloc +import uuid +from datetime import datetime, timezone +from pathlib import Path + +from backend.services.chat_service import ChatService + + +class LegacyStore: + """Pre-optimization unbounded list retained only as a benchmark control.""" + + def __init__(self) -> None: + self.messages: list[dict[str, str]] = [] + + def send_message(self, username: str, content: str) -> None: + self.messages.append( + { + "id": str(uuid.uuid4()), + "username": username, + "content": content, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ) + + +def percentile(values: list[float], fraction: float) -> float: + return sorted(values)[min(len(values) - 1, int(len(values) * fraction))] + + +def measure(factory, samples: int, iterations: int) -> dict[str, float]: + latencies: list[float] = [] + peaks: list[int] = [] + for _ in range(iterations): + store = factory() + tracemalloc.start() + started = time.perf_counter() + for index in range(samples): + store.send_message(f"user-{index % 100}", f"message-{index}") + elapsed = time.perf_counter() - started + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + latencies.append(elapsed * 1_000) + peaks.append(peak) + median = statistics.median(latencies) + return { + "latency_mean_ms": round(statistics.mean(latencies), 3), + "latency_median_ms": round(median, 3), + "latency_p95_ms": round(percentile(latencies, 0.95), 3), + "latency_p99_ms": round(percentile(latencies, 0.99), 3), + "latency_min_ms": round(min(latencies), 3), + "latency_max_ms": round(max(latencies), 3), + "throughput_messages_per_second": round(samples / (median / 1_000), 2), + "peak_memory_mib": round(max(peaks) / 1024 / 1024, 3), + } + + +def run(samples: int = 50_000, iterations: int = 7, retention: int = 10_000) -> dict: + baseline = measure(LegacyStore, samples, iterations) + optimized = measure(lambda: ChatService(retention), samples, iterations) + return { + "schema_version": "1.0.0", + "generated_at_utc": datetime.now(timezone.utc).isoformat(), + "parameters": {"samples": samples, "iterations": iterations, "retention": retention}, + "environment": {"python": platform.python_version(), "platform": platform.platform()}, + "baseline": baseline, + "optimized": optimized, + "comparison": { + "throughput_change_percent": round( + (optimized["throughput_messages_per_second"] / baseline["throughput_messages_per_second"] - 1) * 100, 2 + ), + "peak_memory_change_percent": round( + (optimized["peak_memory_mib"] / baseline["peak_memory_mib"] - 1) * 100, 2 + ), + }, + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--samples", type=int, default=50_000) + parser.add_argument("--iterations", type=int, default=7) + parser.add_argument("--retention", type=int, default=10_000) + parser.add_argument("--output", type=Path, default=Path("benchmarks/latest.json")) + args = parser.parse_args() + if min(args.samples, args.iterations, args.retention) < 1: + parser.error("benchmark values must be positive") + result = run(args.samples, args.iterations, args.retention) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/docs/AUDIT.md b/docs/AUDIT.md new file mode 100644 index 0000000..84ce125 --- /dev/null +++ b/docs/AUDIT.md @@ -0,0 +1,40 @@ +# Production-readiness audit + +## Executive assessment + +TrojanChat contains useful FastAPI, WebSocket, Redis-cache, frontend, container, and supply-chain +building blocks, but its repository layout combines multiple generations and languages without a +single declared support boundary. This change designates `backend.api:app` as the canonical service +and makes its evidence enforceable rather than aspirational. + +## Strengths + +- Dependency-injected AI tests avoid network calls and credentials. +- Non-root multi-stage container with an HTTP health check. +- Dependabot, SBOM, secret scan, Trivy, CodeQL default setup, and tag-driven release automation. +- Cache failures are designed to fail open without blocking inference. + +## Findings and disposition + +| Priority | Finding | Disposition | +|---|---|---| +| P0 | Benchmark workflow reported success and “sub-millisecond” performance without running a benchmark | Replaced with reproducible latency/throughput/memory comparison and artifact | +| P1 | Dependency and vulnerability findings did not fail CI | Audits and HIGH/CRITICAL Trivy findings now block | +| P1 | Frontend lock contained vulnerable Next.js 14.2.15 | Upgraded to 15.5.20; locked npm audit and production build now required | +| P1 | Coverage was reported but had no minimum | Canonical critical path now fails below 90% | +| P1 | Message history grew without bound | Bounded retention with configurable `CHAT_HISTORY_LIMIT` | +| P1 | Message fields and history limit were unbounded | Strict username/content/query validation added | +| P2 | WebSocket router existed but was not mounted | Mounted and integration-tested connection lifecycle | +| P2 | Exceptions were reflected to callers | Replaced with sanitized 503 responses | +| P2 | Multiple duplicate legacy trees obscure ownership | Documented; removal requires a separate migration PR | + +## Residual risk + +- The in-process store is neither durable nor shared. Production multi-instance deployments require + Redis Streams, Postgres, or another durable ordered store with tenant-aware authorization. +- WebSocket authentication, origin policy, rate limiting, message schema enforcement, and TLS + termination remain required before exposing realtime chat publicly. +- Runtime requirements use lower bounds rather than a lock file. Release builds should generate and + review a hashed lock before claiming byte-for-byte reproducibility. +- AI accuracy, safety, precision/recall, and retrieval quality cannot be claimed without a versioned, + representative labeled evaluation dataset. diff --git a/docs/PRODUCTION_CHECKLIST.md b/docs/PRODUCTION_CHECKLIST.md new file mode 100644 index 0000000..d797ab2 --- /dev/null +++ b/docs/PRODUCTION_CHECKLIST.md @@ -0,0 +1,11 @@ +# Production checklist + +- [ ] All CI, CodeQL, secret, dependency, filesystem, and container scans pass for the release SHA. +- [ ] Coverage remains at or above 90% for the canonical critical path. +- [ ] Benchmark regression is within budget and raw artifacts are retained. +- [ ] Image digest, SBOM, provenance, changelog, and rollback target are recorded. +- [ ] TLS, WebSocket authentication/origin policy, API rate limits, and request limits are enforced. +- [ ] Durable shared message storage is deployed, backed up, and restore-tested. +- [ ] Redis/Qdrant health, timeouts, resource limits, logs, metrics, and alerts have owners. +- [ ] A canary validates `/health`, message send/history, WebSocket lifecycle, and AI dependency failure. +- [ ] Previous image and schema/model revisions can be restored within the recovery objective. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b2478cd..8064b0b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,8 @@ "name": "trojanchat-frontend", "version": "1.0.0", "dependencies": { - "next": "14.2.15", + "next": "15.5.20", + "postcss": "8.5.10", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -19,16 +20,492 @@ "typescript": "5.6.3" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@next/env": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", - "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.20.tgz", + "integrity": "sha512-dXh51Wvddf8daEyBXryZZEe1FdVxEWx9lgaTseLZUtC1XP/W8Wri+Z+VPOElHlByk23CyqHdc2oVByX7wsTWsw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", - "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.20.tgz", + "integrity": "sha512-in0yXG7/pRBVjWeEl7f7ZZETpletSMFKXVS4GJgHENTPVrJFNJKPrYewa9rpZcvdjwFece5fZP0CK34G4PxowA==", "cpu": [ "arm64" ], @@ -42,9 +519,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", - "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.20.tgz", + "integrity": "sha512-0hsFshdPnTzGJdDTHeHJ+XPUShOpnyp9pUFDwDhqctsA0Cd8NcIVGRPtptYhgYY9DjkKgCDRkXxmgRc+CgT5Wg==", "cpu": [ "x64" ], @@ -58,15 +535,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", - "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.20.tgz", + "integrity": "sha512-DMvkoBtAABOzE6pMZRW/xNm7sKqql3wzzzZJ1R/d/rp4BCxv6LykouD3tHjGY8WdQqGpZs11t+R9AtjPxvvljw==", "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -77,15 +551,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", - "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.20.tgz", + "integrity": "sha512-RQmDfeYBtXV2FSId7dfA1hE6M/T6+g7wdbYnFQ47tw/gUBwV+CccLVejNmCGa9yLDitk83foeg8hl/3DjfYQ5g==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -96,15 +567,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", - "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.20.tgz", + "integrity": "sha512-DkWLEdKajJwdGt27M3i1VEO2kelTvZrK6Pcb7JvW2BY+nofWm7FBsBNDj7g7Pr1NuQ5PLJvqEqYa20GTsBDnKQ==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -115,15 +583,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", - "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.20.tgz", + "integrity": "sha512-rAO5b7pKHvX+ExdmJskusDXTNbiNZfptifIPZItbUx+AOXxxTydVBsPt7Oz84DRd5mY8e0DcE8kvLj3AIfjE6w==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -134,9 +599,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", - "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.20.tgz", + "integrity": "sha512-Hp3zFsN8N8Kj9+vY6L4vnZ9EtA9eXyATu0q4EfGbZTiocgPUNSfz8NWhym6xvaOmHpJ8EuoypuU1WejCPsTFtg==", "cpu": [ "arm64" ], @@ -149,26 +614,10 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", - "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.20.tgz", + "integrity": "sha512-T/L7CXpR1M0wij/xbF3rT1+7KvSkfOLr7C+ToHHWZTG2eKmb52C5WvsyGCBNtkVvDEUESWkRUbbqSH4rSbOCYQ==", "cpu": [ "x64" ], @@ -181,20 +630,13 @@ "node": ">= 10" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "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==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@types/node": { @@ -235,17 +677,6 @@ "@types/react": "*" } }, - "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/caniuse-lite": { "version": "1.0.30001787", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", @@ -279,11 +710,15 @@ "dev": true, "license": "MIT" }, - "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -322,42 +757,40 @@ } }, "node_modules/next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", - "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.20.tgz", + "integrity": "sha512-cvyS3/geydan1xLtE3FA8VCgdoQ/Gg/dlOldFkFCbB5VcVYJV7090hQLBnvTW2PwT76Z/dHdzDZCsVhZpoOlUA==", "license": "MIT", "dependencies": { - "@next/env": "14.2.15", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", + "@next/env": "15.5.20", + "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1" + "styled-jsx": "5.1.6" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=18.17.0" + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.15", - "@next/swc-darwin-x64": "14.2.15", - "@next/swc-linux-arm64-gnu": "14.2.15", - "@next/swc-linux-arm64-musl": "14.2.15", - "@next/swc-linux-x64-gnu": "14.2.15", - "@next/swc-linux-x64-musl": "14.2.15", - "@next/swc-win32-arm64-msvc": "14.2.15", - "@next/swc-win32-ia32-msvc": "14.2.15", - "@next/swc-win32-x64-msvc": "14.2.15" + "@next/swc-darwin-arm64": "15.5.20", + "@next/swc-darwin-x64": "15.5.20", + "@next/swc-linux-arm64-gnu": "15.5.20", + "@next/swc-linux-arm64-musl": "15.5.20", + "@next/swc-linux-x64-gnu": "15.5.20", + "@next/swc-linux-x64-musl": "15.5.20", + "@next/swc-win32-arm64-msvc": "15.5.20", + "@next/swc-win32-x64-msvc": "15.5.20", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -367,11 +800,42 @@ "@playwright/test": { "optional": true }, + "babel-plugin-react-compiler": { + "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/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -379,9 +843,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -398,9 +862,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -411,6 +875,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -423,6 +888,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -440,6 +906,64 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -449,18 +973,10 @@ "node": ">=0.10.0" } }, - "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/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { "client-only": "0.0.1" @@ -469,7 +985,7 @@ "node": ">= 12.0.0" }, "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "peerDependenciesMeta": { "@babel/core": { diff --git a/frontend/package.json b/frontend/package.json index 2e62475..b88a6ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "lint": "next lint" }, "dependencies": { - "next": "14.2.15", + "next": "15.5.20", + "postcss": "8.5.10", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -19,4 +20,4 @@ "@types/react-dom": "18.3.1", "typescript": "5.6.3" } -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index c76e559..8faf598 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,6 @@ uvicorn>=0.34.0 httpx>=0.28.1 redis>=5.2.0 prometheus-client>=0.21.0 -python-jose[cryptography]>=3.3.0 +PyJWT>=2.12.0 passlib[bcrypt]>=1.7.4 python-json-logger>=2.0.7 diff --git a/tests/test_backend_hardening.py b/tests/test_backend_hardening.py new file mode 100644 index 0000000..c6d997c --- /dev/null +++ b/tests/test_backend_hardening.py @@ -0,0 +1,75 @@ +"""Unit and integration coverage for bounded storage and API trust boundaries.""" + +import asyncio +from unittest.mock import AsyncMock + +import pytest +from backend.api import app +from backend.routes.ws_routes import ConnectionManager +from backend.services.chat_service import ChatService +from httpx import ASGITransport, AsyncClient + + +def test_store_evicts_oldest_message_and_returns_copy() -> None: + service = ChatService(max_messages=2) + service.send_message("a", "one") + service.send_message("b", "two") + service.send_message("c", "three") + history = service.get_messages(50) + assert [message["content"] for message in history] == ["two", "three"] + history.clear() + assert len(service.get_messages(50)) == 2 + + +def test_store_rejects_invalid_capacity_and_nonpositive_limit() -> None: + with pytest.raises(ValueError, match="positive"): + ChatService(max_messages=-1) + assert ChatService(1).get_messages(0) == [] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "payload", + [ + {"username": "", "content": "hello"}, + {"username": "