diff --git a/.gitignore b/.gitignore index ef6c904..b79746c 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ helm/charts/ # CMake и сборка CMakeCache.txt +CMakeLists.txt CMakeFiles/ CMakeScripts/ Makefile diff --git a/LICENSE b/LICENSE deleted file mode 100644 index cc42e2a..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Finnik & S1gmagor - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Readme.md b/Readme.md index ad9f586..77b1820 100644 --- a/Readme.md +++ b/Readme.md @@ -2,21 +2,31 @@
-![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) -![Status](https://img.shields.io/badge/Status-Active-success.svg) +[![GitHub stars](https://img.shields.io/github/stars/CS151512/STREMO?style=social)](https://github.com/CS151512/STREMO) +[![Licence](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](./LICENSE)
-![C++](https://img.shields.io/badge/C++-00599C?style=flat&logo=c%2B%2B&logoColor=white) -![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) -![Next.js](https://img.shields.io/badge/Next.js-000000?style=flat&logo=nextdotjs&logoColor=white) -![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat&logo=docker&logoColor=white) -![K3s](https://img.shields.io/badge/K3s-FFC61C?style=flat&logo=k3s&logoColor=black) -![Terraform](https://img.shields.io/badge/Terraform-844FBA?style=flat&logo=terraform&logoColor=white) -![Apache Kafka](https://img.shields.io/badge/Kafka-231F20?style=flat&logo=apachekafka&logoColor=white) -![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?style=flat&logo=postgresql&logoColor=white) -![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat&logo=redis&logoColor=white) -![Git](https://img.shields.io/badge/Git-F05032?style=flat&logo=git&logoColor=white) +![Figma](https://img.shields.io/badge/figma-%23F24E1E.svg?style=for-the-badge&logo=figma&logoColor=white) +![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white) +![ClickHouse](https://img.shields.io/badge/ClickHouse-FFCC01?style=for-the-badge&logo=clickhouse&logoColor=white) +![C++](https://img.shields.io/badge/c++-%2300599C.svg?style=for-the-badge&logo=c%2B%2B&logoColor=white) +![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) +![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) +![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white) +![Terraform](https://img.shields.io/badge/terraform-%235835CC.svg?style=for-the-badge&logo=terraform&logoColor=white) +![Apache Kafka](https://img.shields.io/badge/Apache%20Kafka-000?style=for-the-badge&logo=apachekafka) +![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) +![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white) +![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) +![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white) +![Next JS](https://img.shields.io/badge/Next-black.svg?style=for-the-badge&logo=next.js&logoColor=white) +![LaTeX](https://img.shields.io/badge/latex-%23008080.svg?style=for-the-badge&logo=latex&logoColor=white) +![Postman](https://img.shields.io/badge/Postman-FF6C37?style=for-the-badge&logo=postman&logoColor=white) +![Grafana](https://img.shields.io/badge/grafana-%23F46800.svg?style=for-the-badge&logo=grafana&logoColor=white) +![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=Prometheus&logoColor=white) +![CMake](https://img.shields.io/badge/CMake-%23008FBA.svg?style=for-the-badge&logo=cmake&logoColor=white) +![Nginx](https://img.shields.io/badge/nginx-%23009639.svg?style=for-the-badge&logo=nginx&logoColor=white)
@@ -144,4 +154,7 @@ pdflatex main.tex ``` --- -**by finnik & s1gmagor** +**by finnik && s1gmagor** + +* **Оригинальный дизайн:** [где-то Dribble, я просто уже не могу найти :(](https://dribbble.com/search/shots/popular/mobile?q=streaming-service) +* **Статус:** Данный проект разработан исключительно в образовательных целях (университетская расчетно-графическая работа) и не имеет коммерческой выгоды. Права на исходный визуальный концепт принадлежат его автору. Не надо бить палками... diff --git a/backend/service/auth-service/CMakeLists.txt b/backend/service/auth-service/CMakeLists.txt deleted file mode 100644 index 3e74937..0000000 --- a/backend/service/auth-service/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(auth_service) - -add_executable(auth_service src/main.cpp) -target_link_libraries(auth_service PRIVATE cpp_core) diff --git a/backend/service/auth-service/include/service/logic.hpp b/backend/service/auth-service/include/service/logic.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/service/manager.hpp b/backend/service/auth-service/include/service/manager.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/service/validator.hpp b/backend/service/auth-service/include/service/validator.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/utils/argon2_hasher.hpp b/backend/service/auth-service/include/utils/argon2_hasher.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/utils/config_parser.hpp b/backend/service/auth-service/include/utils/config_parser.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/utils/jwt_signer.hpp b/backend/service/auth-service/include/utils/jwt_signer.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/__init__.py b/backend/service/auth-service/src/__init__.py new file mode 100644 index 0000000..098642c --- /dev/null +++ b/backend/service/auth-service/src/__init__.py @@ -0,0 +1,2 @@ +"""auth-service - microservice for authentication & authorization""" +__version__ = "0.1.0" \ No newline at end of file diff --git a/backend/service/auth-service/src/api/__init__.py b/backend/service/auth-service/src/api/__init__.py new file mode 100644 index 0000000..1bdda86 --- /dev/null +++ b/backend/service/auth-service/src/api/__init__.py @@ -0,0 +1 @@ +"""API: HTTP & gRPC interfaces""" \ No newline at end of file diff --git a/backend/service/auth-service/include/api/grpc/handlers.hpp b/backend/service/auth-service/src/api/dependencies.py similarity index 100% rename from backend/service/auth-service/include/api/grpc/handlers.hpp rename to backend/service/auth-service/src/api/dependencies.py diff --git a/backend/service/auth-service/src/api/grpc/__init__.py b/backend/service/auth-service/src/api/grpc/__init__.py new file mode 100644 index 0000000..5424fac --- /dev/null +++ b/backend/service/auth-service/src/api/grpc/__init__.py @@ -0,0 +1 @@ +"""gRPC API""" \ No newline at end of file diff --git a/backend/service/auth-service/src/api/grpc/handlers.cpp b/backend/service/auth-service/src/api/grpc/handlers.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/api/grpc/server.hpp b/backend/service/auth-service/src/api/grpc/handlers.py similarity index 100% rename from backend/service/auth-service/include/api/grpc/server.hpp rename to backend/service/auth-service/src/api/grpc/handlers.py diff --git a/backend/service/auth-service/src/api/grpc/server.cpp b/backend/service/auth-service/src/api/grpc/server.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/api/grpc_handler.hpp b/backend/service/auth-service/src/api/grpc/server.py similarity index 100% rename from backend/service/auth-service/include/api/grpc_handler.hpp rename to backend/service/auth-service/src/api/grpc/server.py diff --git a/backend/service/auth-service/src/api/grpc_handler.cpp b/backend/service/auth-service/src/api/grpc_handler.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/api/http/__init__.py b/backend/service/auth-service/src/api/http/__init__.py new file mode 100644 index 0000000..35b3a90 --- /dev/null +++ b/backend/service/auth-service/src/api/http/__init__.py @@ -0,0 +1 @@ +"""HTTP API""" diff --git a/backend/service/auth-service/include/errors/exceptions.hpp b/backend/service/auth-service/src/api/http/routers.py similarity index 100% rename from backend/service/auth-service/include/errors/exceptions.hpp rename to backend/service/auth-service/src/api/http/routers.py diff --git a/backend/service/auth-service/include/infrastructure/kafka/consumer.hpp b/backend/service/auth-service/src/api/http/server.py similarity index 100% rename from backend/service/auth-service/include/infrastructure/kafka/consumer.hpp rename to backend/service/auth-service/src/api/http/server.py diff --git a/backend/service/auth-service/src/core/__init__.py b/backend/service/auth-service/src/core/__init__.py new file mode 100644 index 0000000..1d0aeb6 --- /dev/null +++ b/backend/service/auth-service/src/core/__init__.py @@ -0,0 +1 @@ +"""Core module: configuration, security & exceptions""" \ No newline at end of file diff --git a/backend/service/auth-service/src/core/config.py b/backend/service/auth-service/src/core/config.py new file mode 100644 index 0000000..2d787d9 --- /dev/null +++ b/backend/service/auth-service/src/core/config.py @@ -0,0 +1,61 @@ +from functools import lru_cache +from pydantic import PostgresDsn, RedisDsn, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + ) + + # App + APP_NAME: str = "auth-service" + APP_ENV: str = "development" + DEBUG: bool = False + + # HTTP Server + HTTP_HOST: str = "0.0.0.0" + HTTP_PORT: int = 8000 + + # gRPC Server + GRPC_HOST: str = "0.0.0.0" + GRPC_PORT: int = 50051 + + # JWT + JWT_SECRET_KEY: str + JWT_ALGORITHM: str = "HS256" + JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7 + + # PostgreSQL + POSTGRES_DSN: PostgresDsn + + # Redis + REDIS_DSN: RedisDsn + + # Kafka + KAFKA_BOOTSTRAP_SERVERS: str + KAFKA_TOPIC_USER_EVENTS: str = "user-events" + KAFKA_CONSUMER_GROUP_ID: str = "auth-service-group" + + # Password hashing + BCRYPT_ROUNDS: int = 12 + + @field_validator("APP_ENV") + @classmethod + def validate_app_env(cls, v: str) -> str: + allowed = {"development", "staging", "production"} + if v not in allowed: + raise ValueError(f"APP_ENV must be one of {allowed}") + return v + + @property + def is_production(self) -> bool: + return self.APP_ENV == "production" + + +@lru_cache +def get_settings() -> Settings: + return Settings() \ No newline at end of file diff --git a/backend/service/auth-service/src/core/exceptions.py b/backend/service/auth-service/src/core/exceptions.py new file mode 100644 index 0000000..e223416 --- /dev/null +++ b/backend/service/auth-service/src/core/exceptions.py @@ -0,0 +1,70 @@ +from http import HTTPStatus + + +class AppException(Exception): + """Base application exception.""" + + status_code: int = HTTPStatus.INTERNAL_SERVER_ERROR + detail: str = "An unexpected error occurred." + + def __init__(self, detail: str | None = None) -> None: + self.detail = detail or self.__class__.detail + super().__init__(self.detail) + + +# --- Auth --- + +class AuthException(AppException): + """Base auth exception.""" + + +class InvalidCredentialsError(AuthException): + status_code = HTTPStatus.UNAUTHORIZED + detail = "Invalid email or password." + + +class InvalidTokenError(AuthException): + status_code = HTTPStatus.UNAUTHORIZED + detail = "Token is invalid or has expired." + + +class TokenExpiredError(AuthException): + status_code = HTTPStatus.UNAUTHORIZED + detail = "Token has expired." + + +class InsufficientPermissionsError(AuthException): + status_code = HTTPStatus.FORBIDDEN + detail = "You do not have permission to perform this action." + + +# --- User --- + +class UserException(AppException): + """Base user exception.""" + + +class UserNotFoundError(UserException): + status_code = HTTPStatus.NOT_FOUND + detail = "User not found." + + +class UserAlreadyExistsError(UserException): + status_code = HTTPStatus.CONFLICT + detail = "A user with this email already exists." + + +class UserInactiveError(UserException): + status_code = HTTPStatus.FORBIDDEN + detail = "This user account is inactive." + + +# --- Validation --- + +class ValidationException(AppException): + status_code = HTTPStatus.UNPROCESSABLE_ENTITY + detail = "Validation failed." + + +class WeakPasswordError(ValidationException): + detail = "Password does not meet the minimum security requirements." \ No newline at end of file diff --git a/backend/service/auth-service/src/core/security.py b/backend/service/auth-service/src/core/security.py new file mode 100644 index 0000000..d83c3be --- /dev/null +++ b/backend/service/auth-service/src/core/security.py @@ -0,0 +1,91 @@ +from datetime import datetime, timedelta, timezone +from typing import Any +from uuid import UUID + +import bcrypt +from jose import JWTError, jwt + +from src.core.config import get_settings +from src.core.exceptions import InvalidTokenError, TokenExpiredError + +settings = get_settings() + + +# PASSWORD HASHING +def hash_password(plain: str) -> str: + """Return a bcrypt hash of *plain*.""" + salt = bcrypt.gensalt(rounds=settings.BCRYPT_ROUNDS) + return bcrypt.hashpw(plain.encode(), salt).decode() + + +def verify_password(plain: str, hashed: str) -> bool: + """Return True if *plain* matches *hashed*.""" + return bcrypt.checkpw(plain.encode(), hashed.encode()) + + +# JWT +_ACCESS = "access" +_REFRESH = "refresh" + + +def _create_token( + subject: str | UUID, + token_type: str, + expires_delta: timedelta, + extra_claims: dict[str, Any] | None = None, +) -> str: + now = datetime.now(tz=timezone.utc) + payload: dict[str, Any] = { + "sub": str(subject), + "type": token_type, + "iat": now, + "exp": now + expires_delta, + } + if extra_claims: + payload.update(extra_claims) + return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) + + +def create_access_token(user_id: str | UUID, extra_claims: dict[str, Any] | None = None) -> str: + return _create_token( + subject=user_id, + token_type=_ACCESS, + expires_delta=timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES), + extra_claims=extra_claims, + ) + + +def create_refresh_token(user_id: str | UUID) -> str: + return _create_token( + subject=user_id, + token_type=_REFRESH, + expires_delta=timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS), + ) + + +def decode_token(token: str, expected_type: str = _ACCESS) -> dict[str, Any]: + """ + Decode and validate *token*. + Raises: + TokenExpiredError: if the token is past its expiry. + InvalidTokenError: if the token is malformed or has an unexpected type. + """ + try: + payload = jwt.decode( + token, + settings.JWT_SECRET_KEY, + algorithms=[settings.JWT_ALGORITHM], + ) + except jwt.ExpiredSignatureError: + raise TokenExpiredError() + except JWTError: + raise InvalidTokenError() + + if payload.get("type") != expected_type: + raise InvalidTokenError("Unexpected token type.") + + return payload + + +def decode_refresh_token(token: str) -> dict[str, Any]: + return decode_token(token, expected_type=_REFRESH) diff --git a/backend/service/auth-service/src/domain/__init__.py b/backend/service/auth-service/src/domain/__init__.py new file mode 100644 index 0000000..b57cce8 --- /dev/null +++ b/backend/service/auth-service/src/domain/__init__.py @@ -0,0 +1,5 @@ +"""Domain: Business entities""" + +from src.domain.entities import RefreshToken, User, UserRole, UserStatus + +__all__ = ["User", "UserRole", "UserStatus", "RefreshToken"] \ No newline at end of file diff --git a/backend/service/auth-service/src/domain/entities.py b/backend/service/auth-service/src/domain/entities.py new file mode 100644 index 0000000..a4f0bcb --- /dev/null +++ b/backend/service/auth-service/src/domain/entities.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import StrEnum +from uuid import UUID, uuid4 + + +class UserRole(StrEnum): + ADMIN = "admin" + USER = "user" + SERVICE = "service" + + +class UserStatus(StrEnum): + ACTIVE = "active" + INACTIVE = "inactive" + BANNED = "banned" + PENDING_VERIFICATION = "pending_verification" + + +@dataclass +class User: + """Core user entity — framework-agnostic""" + + email: str + hashed_password: str + + id: UUID = field(default_factory=uuid4) + role: UserRole = UserRole.USER + status: UserStatus = UserStatus.PENDING_VERIFICATION + created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc)) + updated_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc)) + + # Optional profile fields + first_name: str | None = None + last_name: str | None = None + + @property + def is_active(self) -> bool: + return self.status == UserStatus.ACTIVE + + @property + def is_admin(self) -> bool: + return self.role == UserRole.ADMIN + + @property + def full_name(self) -> str | None: + parts = filter(None, [self.first_name, self.last_name]) + return " ".join(parts) or None + + def activate(self) -> None: + self.status = UserStatus.ACTIVE + self._touch() + + def deactivate(self) -> None: + self.status = UserStatus.INACTIVE + self._touch() + + def ban(self) -> None: + self.status = UserStatus.BANNED + self._touch() + + def _touch(self) -> None: + self.updated_at = datetime.now(tz=timezone.utc) + + +@dataclass +class RefreshToken: + """Persisted refresh token for rotation / revocation""" + + token: str + user_id: UUID + expires_at: datetime + + id: UUID = field(default_factory=uuid4) + created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc)) + revoked: bool = False + + @property + def is_expired(self) -> bool: + return datetime.now(tz=timezone.utc) >= self.expires_at + + @property + def is_valid(self) -> bool: + return not self.revoked and not self.is_expired + + def revoke(self) -> None: + self.revoked = True \ No newline at end of file diff --git a/backend/service/auth-service/src/errors/exceptions.cpp b/backend/service/auth-service/src/errors/exceptions.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/infrastructure/__init__.py b/backend/service/auth-service/src/infrastructure/__init__.py new file mode 100644 index 0000000..92ef1b0 --- /dev/null +++ b/backend/service/auth-service/src/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Infrastructure: external services & storage""" \ No newline at end of file diff --git a/backend/service/auth-service/src/infrastructure/kafka/__init__.py b/backend/service/auth-service/src/infrastructure/kafka/__init__.py new file mode 100644 index 0000000..030da62 --- /dev/null +++ b/backend/service/auth-service/src/infrastructure/kafka/__init__.py @@ -0,0 +1 @@ +"""Kafka: event streaming""" \ No newline at end of file diff --git a/backend/service/auth-service/src/infrastructure/kafka/consumer.cpp b/backend/service/auth-service/src/infrastructure/kafka/consumer.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/infrastructure/kafka/producer.hpp b/backend/service/auth-service/src/infrastructure/kafka/consumer.py similarity index 100% rename from backend/service/auth-service/include/infrastructure/kafka/producer.hpp rename to backend/service/auth-service/src/infrastructure/kafka/consumer.py diff --git a/backend/service/auth-service/src/infrastructure/kafka/producer.cpp b/backend/service/auth-service/src/infrastructure/kafka/producer.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/models/domain.hpp b/backend/service/auth-service/src/infrastructure/kafka/producer.py similarity index 100% rename from backend/service/auth-service/include/models/domain.hpp rename to backend/service/auth-service/src/infrastructure/kafka/producer.py diff --git a/backend/service/auth-service/src/infrastructure/postgres/__init__.py b/backend/service/auth-service/src/infrastructure/postgres/__init__.py new file mode 100644 index 0000000..6e8ae75 --- /dev/null +++ b/backend/service/auth-service/src/infrastructure/postgres/__init__.py @@ -0,0 +1 @@ +"""PostgreSQL DB""" \ No newline at end of file diff --git a/backend/service/auth-service/include/models/domain_types.hpp b/backend/service/auth-service/src/infrastructure/postgres/database.py similarity index 100% rename from backend/service/auth-service/include/models/domain_types.hpp rename to backend/service/auth-service/src/infrastructure/postgres/database.py diff --git a/backend/service/auth-service/include/models/dto.hpp b/backend/service/auth-service/src/infrastructure/postgres/models.py similarity index 100% rename from backend/service/auth-service/include/models/dto.hpp rename to backend/service/auth-service/src/infrastructure/postgres/models.py diff --git a/backend/service/auth-service/src/infrastructure/redis/__init__.py b/backend/service/auth-service/src/infrastructure/redis/__init__.py new file mode 100644 index 0000000..70b3ff9 --- /dev/null +++ b/backend/service/auth-service/src/infrastructure/redis/__init__.py @@ -0,0 +1 @@ +"""Redis client for caching & sessions""" \ No newline at end of file diff --git a/backend/service/auth-service/include/repository/postgres.hpp b/backend/service/auth-service/src/infrastructure/redis/client.py similarity index 100% rename from backend/service/auth-service/include/repository/postgres.hpp rename to backend/service/auth-service/src/infrastructure/redis/client.py diff --git a/backend/service/auth-service/src/main.cpp b/backend/service/auth-service/src/main.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/main.py b/backend/service/auth-service/src/main.py new file mode 100644 index 0000000..ba9e7cd --- /dev/null +++ b/backend/service/auth-service/src/main.py @@ -0,0 +1,145 @@ +import asyncio +import logging +from contextlib import asynccontextmanager +from typing import AsyncIterator + +import uvicorn +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from src import __version__ +from src.core.config import get_settings +from src.core.exceptions import AppException + +logger = logging.getLogger(__name__) +settings = get_settings() + + +# Lifespan — startup / shutdown of shared resources + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """ + Initialise infrastructure clients on startup and cleanly shut them down. + All resources are stored in app.state so routers and dependencies can + reach them without importing globals. + """ + from src.infrastructure.postgres.database import Database + from src.infrastructure.redis.client import RedisClient + from src.infrastructure.kafka.producer import KafkaProducer + from src.infrastructure.kafka.consumer import KafkaConsumer + + logger.info("Starting %s v%s [%s]", settings.APP_NAME, __version__, settings.APP_ENV) + + # Postgres + db = Database(dsn=str(settings.POSTGRES_DSN)) + await db.connect() + app.state.db = db + + # Redis + redis = RedisClient(dsn=str(settings.REDIS_DSN)) + await redis.connect() + app.state.redis = redis + + # Kafka producer + producer = KafkaProducer(bootstrap_servers=settings.KAFKA_BOOTSTRAP_SERVERS) + await producer.start() + app.state.kafka_producer = producer + + # Kafka consumer (runs as a background task) + consumer = KafkaConsumer( + bootstrap_servers=settings.KAFKA_BOOTSTRAP_SERVERS, + topic=settings.KAFKA_TOPIC_USER_EVENTS, + group_id=settings.KAFKA_CONSUMER_GROUP_ID, + ) + consumer_task = asyncio.create_task(consumer.start(), name="kafka-consumer") + app.state.kafka_consumer = consumer + app.state.kafka_consumer_task = consumer_task + + logger.info("All infrastructure clients ready.") + + yield # ── application is running ── + + logger.info("Shutting down %s…", settings.APP_NAME) + + consumer_task.cancel() + try: + await consumer_task + except asyncio.CancelledError: + pass + + await producer.stop() + await redis.disconnect() + await db.disconnect() + + logger.info("Shutdown complete.") + + +# FastAPI application factory +def create_app() -> FastAPI: + app = FastAPI( + title=settings.APP_NAME, + version=__version__, + docs_url="/docs" if not settings.is_production else None, + redoc_url="/redoc" if not settings.is_production else None, + lifespan=lifespan, + ) + + # ── Exception handlers ────────────────────────────────────────────────── + + @app.exception_handler(AppException) + async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse: + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, + ) + + # ── Routers ───────────────────────────────────────────────────────────── + + from src.api.http.routers import router as http_router + app.include_router(http_router, prefix="/api/v1") + + # ── Health check ──────────────────────────────────────────────────────── + + @app.get("/health", tags=["meta"], include_in_schema=False) + async def health() -> dict: + return {"status": "ok", "version": __version__} + + return app + + +# Entrypoint +app = create_app() + + +async def _serve_grpc() -> None: + """Start the gRPC server alongside the HTTP server.""" + from src.api.grpc.server import GrpcServer + server = GrpcServer(host=settings.GRPC_HOST, port=settings.GRPC_PORT) + await server.start() + logger.info("gRPC server listening on %s:%s", settings.GRPC_HOST, settings.GRPC_PORT) + await server.wait_for_termination() + + +async def _serve_http() -> None: + config = uvicorn.Config( + app=app, + host=settings.HTTP_HOST, + port=settings.HTTP_PORT, + log_level="debug" if settings.DEBUG else "info", + loop="none", # we manage the event loop ourselves + ) + server = uvicorn.Server(config) + await server.serve() + + +async def main() -> None: + logging.basicConfig( + level=logging.DEBUG if settings.DEBUG else logging.INFO, + format="%(asctime)s %(levelname)-8s %(name)s — %(message)s", + ) + await asyncio.gather(_serve_http(), _serve_grpc()) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/service/auth-service/src/repositories/__init__.py b/backend/service/auth-service/src/repositories/__init__.py new file mode 100644 index 0000000..7653ca8 --- /dev/null +++ b/backend/service/auth-service/src/repositories/__init__.py @@ -0,0 +1 @@ +"""Repository: data access""" \ No newline at end of file diff --git a/backend/service/auth-service/include/repository/postgres/client.hpp b/backend/service/auth-service/src/repositories/base.py similarity index 100% rename from backend/service/auth-service/include/repository/postgres/client.hpp rename to backend/service/auth-service/src/repositories/base.py diff --git a/backend/service/auth-service/include/repository/postgres/queries.hpp b/backend/service/auth-service/src/repositories/user.py similarity index 100% rename from backend/service/auth-service/include/repository/postgres/queries.hpp rename to backend/service/auth-service/src/repositories/user.py diff --git a/backend/service/auth-service/src/repository/postgres.cpp b/backend/service/auth-service/src/repository/postgres.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/repository/postgres/client.cpp b/backend/service/auth-service/src/repository/postgres/client.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/repository/postgres/queries.cpp b/backend/service/auth-service/src/repository/postgres/queries.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/repository/redis/client.cpp b/backend/service/auth-service/src/repository/redis/client.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/schemes/__init__.py b/backend/service/auth-service/src/schemes/__init__.py new file mode 100644 index 0000000..4ff7ea3 --- /dev/null +++ b/backend/service/auth-service/src/schemes/__init__.py @@ -0,0 +1,20 @@ +"""Schemes: dtos & data validation models""" + +from src.schemes.dto import ( + AccessTokenResponse, + LoginRequest, + LogoutRequest, + RefreshRequest, + RegisterRequest, + RegisterResponse, + TokenPair, + TokenPayload, + UserCreatedEvent, + UserResponse, +) + +__all__ = [ + "RegisterRequest", "LoginRequest", "RefreshRequest", "LogoutRequest", + "TokenPair", "AccessTokenResponse", "RegisterResponse", + "UserResponse", "TokenPayload", "UserCreatedEvent", +] \ No newline at end of file diff --git a/backend/service/auth-service/src/schemes/dto.py b/backend/service/auth-service/src/schemes/dto.py new file mode 100644 index 0000000..63d11e2 --- /dev/null +++ b/backend/service/auth-service/src/schemes/dto.py @@ -0,0 +1,98 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, EmailStr, Field, field_validator + +from src.domain.entities import UserRole, UserStatus + + +# SHARED BASE +class CamelModel(BaseModel): + """Base model that serialises to camelCase for HTTP responses.""" + + model_config = {"populate_by_name": True, "use_enum_values": True} + + +# AUTH REQUESTS +class RegisterRequest(CamelModel): + email: EmailStr + password: str = Field(min_length=8, max_length=128) + first_name: str | None = Field(default=None, max_length=64) + last_name: str | None = Field(default=None, max_length=64) + + @field_validator("password") + @classmethod + def password_not_blank(cls, v: str) -> str: + if v.strip() == "": + raise ValueError("Password must not be blank.") + return v + + +class LoginRequest(CamelModel): + email: EmailStr + password: str + + +class RefreshRequest(CamelModel): + refresh_token: str + + +class LogoutRequest(CamelModel): + refresh_token: str + + +# AUTH RESPONSES +class TokenPair(CamelModel): + access_token: str + refresh_token: str + token_type: str = "bearer" + expires_in: int # seconds until access token expires + + +class AccessTokenResponse(CamelModel): + access_token: str + token_type: str = "bearer" + expires_in: int + + +# USER RESPONSES +class UserResponse(CamelModel): + id: UUID + email: EmailStr + role: UserRole + status: UserStatus + first_name: str | None + last_name: str | None + created_at: datetime + updated_at: datetime + + @property + def full_name(self) -> str | None: + parts = filter(None, [self.first_name, self.last_name]) + return " ".join(parts) or None + + +class RegisterResponse(CamelModel): + user: UserResponse + tokens: TokenPair + + +# SERVICE-TO-SERVICE DTOS + +class TokenPayload(CamelModel): + """Decoded JWT payload passed between internal layers.""" + + sub: UUID + type: str + iat: datetime + exp: datetime + role: UserRole = UserRole.USER + + +class UserCreatedEvent(CamelModel): + """Kafka event emitted after successful registration.""" + + event: str = "user.created" + user_id: UUID + email: EmailStr + occurred_at: datetime \ No newline at end of file diff --git a/backend/service/auth-service/src/service/logic.cpp b/backend/service/auth-service/src/service/logic.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/service/manager.cpp b/backend/service/auth-service/src/service/manager.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/service/validator.cpp b/backend/service/auth-service/src/service/validator.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/services/__init__.py b/backend/service/auth-service/src/services/__init__.py new file mode 100644 index 0000000..a5a7f92 --- /dev/null +++ b/backend/service/auth-service/src/services/__init__.py @@ -0,0 +1,5 @@ +"""Services: business logic""" + +from src.services.auth import AuthService + +__all__ = ["AuthService"] \ No newline at end of file diff --git a/backend/service/auth-service/src/services/auth.py b/backend/service/auth-service/src/services/auth.py new file mode 100644 index 0000000..dd7f300 --- /dev/null +++ b/backend/service/auth-service/src/services/auth.py @@ -0,0 +1,194 @@ +from datetime import datetime, timedelta, timezone +from uuid import UUID + +from src.core.config import get_settings +from src.core.exceptions import ( + InvalidCredentialsError, + InvalidTokenError, + UserAlreadyExistsError, + UserInactiveError, + UserNotFoundError, +) +from src.core.security import ( + create_access_token, + create_refresh_token, + decode_refresh_token, + hash_password, + verify_password, +) +from src.domain.entities import RefreshToken, User +from src.schemes.dto import ( + AccessTokenResponse, + LoginRequest, + RefreshRequest, + RegisterRequest, + RegisterResponse, + TokenPair, + UserCreatedEvent, + UserResponse, +) +from src.services.validators import validate_password_strength + +settings = get_settings() + + +class AuthService: + """ + Orchestrates authentication flows: + - register, login, token refresh, logout + Depends on abstract repository and infrastructure interfaces so it + remains fully unit-testable without real I/O. + """ + + def __init__( + self, + user_repo, # repositories.user.UserRepository + token_repo, # repositories.base.BaseRepository[RefreshToken] + redis_client, # infrastructure.redis.client.RedisClient + kafka_producer, # infrastructure.kafka.producer.KafkaProducer + ) -> None: + self._users = user_repo + self._tokens = token_repo + self._redis = redis_client + self._kafka = kafka_producer + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + async def register(self, data: RegisterRequest) -> RegisterResponse: + """Create a new user account and return a token pair.""" + await self._assert_email_available(data.email) + validate_password_strength(data.password) + + user = User( + email=data.email, + hashed_password=hash_password(data.password), + first_name=data.first_name, + last_name=data.last_name, + ) + user.activate() + await self._users.save(user) + + tokens = await self._issue_token_pair(user) + await self._publish_user_created(user) + + return RegisterResponse( + user=self._to_user_response(user), + tokens=tokens, + ) + + async def login(self, data: LoginRequest) -> TokenPair: + """Authenticate with email + password and return a token pair.""" + user = await self._users.get_by_email(data.email) + if user is None or not verify_password(data.password, user.hashed_password): + raise InvalidCredentialsError() + + if not user.is_active: + raise UserInactiveError() + + return await self._issue_token_pair(user) + + async def refresh(self, data: RefreshRequest) -> AccessTokenResponse: + """ + Rotate refresh token and return a new access token. + + Uses a one-time-use pattern: the old refresh token is revoked and a + new one is persisted in its place (refresh token rotation). + """ + payload = decode_refresh_token(data.refresh_token) + user_id: UUID = UUID(payload["sub"]) + + stored: RefreshToken | None = await self._tokens.get_by_token(data.refresh_token) + if stored is None or not stored.is_valid: + # Possible token reuse attack — revoke all tokens for this user. + await self._tokens.revoke_all_for_user(user_id) + raise InvalidTokenError("Refresh token is invalid or has been revoked.") + + user = await self._users.get_by_id(user_id) + if user is None: + raise UserNotFoundError() + if not user.is_active: + raise UserInactiveError() + + stored.revoke() + await self._tokens.save(stored) + + new_access = create_access_token(user.id, extra_claims={"role": user.role}) + new_refresh = create_refresh_token(user.id) + await self._persist_refresh_token(user.id, new_refresh) + + # Blacklist the old token in Redis (belt-and-suspenders) + await self._blacklist_token(data.refresh_token) + + return AccessTokenResponse( + access_token=new_access, + expires_in=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60, + ) + + async def logout(self, refresh_token: str) -> None: + """Revoke the supplied refresh token.""" + stored: RefreshToken | None = await self._tokens.get_by_token(refresh_token) + if stored is not None: + stored.revoke() + await self._tokens.save(stored) + await self._blacklist_token(refresh_token) + + async def logout_all(self, user_id: UUID) -> None: + """Revoke all active sessions for *user_id*.""" + await self._tokens.revoke_all_for_user(user_id) + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + async def _assert_email_available(self, email: str) -> None: + existing = await self._users.get_by_email(email) + if existing is not None: + raise UserAlreadyExistsError() + + async def _issue_token_pair(self, user: User) -> TokenPair: + access = create_access_token(user.id, extra_claims={"role": user.role}) + refresh = create_refresh_token(user.id) + await self._persist_refresh_token(user.id, refresh) + return TokenPair( + access_token=access, + refresh_token=refresh, + expires_in=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60, + ) + + async def _persist_refresh_token(self, user_id: UUID, token: str) -> None: + expires_at = datetime.now(tz=timezone.utc) + timedelta( + days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS + ) + record = RefreshToken(token=token, user_id=user_id, expires_at=expires_at) + await self._tokens.save(record) + + async def _blacklist_token(self, token: str) -> None: + ttl = settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 86_400 + await self._redis.setex(f"blacklist:{token}", ttl, "1") + + async def _publish_user_created(self, user: User) -> None: + event = UserCreatedEvent( + user_id=user.id, + email=user.email, + occurred_at=user.created_at, + ) + await self._kafka.produce( + topic=settings.KAFKA_TOPIC_USER_EVENTS, + key=str(user.id), + value=event.model_dump_json(), + ) + + @staticmethod + def _to_user_response(user: User) -> UserResponse: + return UserResponse( + id=user.id, + email=user.email, + role=user.role, + status=user.status, + first_name=user.first_name, + last_name=user.last_name, + created_at=user.created_at, + updated_at=user.updated_at, + ) diff --git a/backend/service/auth-service/src/services/validators.py b/backend/service/auth-service/src/services/validators.py new file mode 100644 index 0000000..43a9f0f --- /dev/null +++ b/backend/service/auth-service/src/services/validators.py @@ -0,0 +1,55 @@ +import re + +from src.core.exceptions import WeakPasswordError + +# At least: 1 uppercase, 1 lowercase, 1 digit, 1 special char +_PASSWORD_RE = re.compile( + r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]).{8,128}$" +) + +_COMMON_PASSWORDS = { + "Password1!", + "Password123!", + "Qwerty123!", + "Welcome1!", + "Admin123!", +} + + +def validate_password_strength(password: str) -> None: + """ + Enforce password strength rules. + Rules: + - 8–128 characters + - At least one uppercase letter + - At least one lowercase letter + - At least one digit + - At least one special character + - Not a commonly used password + Raises: + WeakPasswordError: when any rule is violated. + """ + if password in _COMMON_PASSWORDS: + raise WeakPasswordError("This password is too common.") + + if not _PASSWORD_RE.match(password): + raise WeakPasswordError( + "Password must be 8–128 characters and contain at least one uppercase letter, " + "one lowercase letter, one digit, and one special character." + ) + + +def validate_email_domain(email: str, blocked_domains: set[str] | None = None) -> None: + """ + Optionally reject sign-ups from disposable / blocked email domains. + Raises: + ValidationException: when the email domain is blocked. + """ + if not blocked_domains: + return + + from src.core.exceptions import ValidationException + + domain = email.split("@")[-1].lower() + if domain in blocked_domains: + raise ValidationException(f"Sign-ups from '{domain}' are not allowed.") diff --git a/backend/service/auth-service/src/utils/argon2_hasher.cpp b/backend/service/auth-service/src/utils/argon2_hasher.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/utils/config_parser.cpp b/backend/service/auth-service/src/utils/config_parser.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/src/utils/jwt_signer.cpp b/backend/service/auth-service/src/utils/jwt_signer.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/auth-service/include/repository/redis/client.hpp b/backend/service/auth-service/tests/__init__.py similarity index 100% rename from backend/service/auth-service/include/repository/redis/client.hpp rename to backend/service/auth-service/tests/__init__.py diff --git a/backend/service/auth-service/tests/conftest.py b/backend/service/auth-service/tests/conftest.py new file mode 100644 index 0000000..1776c4a --- /dev/null +++ b/backend/service/auth-service/tests/conftest.py @@ -0,0 +1,221 @@ +""" +Shared pytest fixtures for unit and integration tests. +Fixture scopes at a glance: + - session : real DB / Redis / Kafka connections (integration only) + - function : in-memory fakes and mocked services (unit tests, default) +""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timedelta, timezone +from typing import AsyncIterator +from unittest.mock import AsyncMock +from uuid import uuid4 + +import pytest +import pytest_asyncio + +from src.core.config import Settings +from src.domain.entities import RefreshToken, User, UserRole, UserStatus +from src.schemes.dto import LoginRequest, RefreshRequest, RegisterRequest + + +# EVENT LOOP +@pytest.fixture(scope="session") +def event_loop(): + """Single event loop for the whole test session.""" + policy = asyncio.get_event_loop_policy() + loop = policy.new_event_loop() + yield loop + loop.close() + + +# SETTINGS OVERRIDE — ALWAYS USE TEST VALUES, NEVER PRODUCTION SECRETS +@pytest.fixture(scope="session") +def test_settings() -> Settings: + return Settings( + APP_ENV="development", + DEBUG=True, + JWT_SECRET_KEY="test-secret-key-not-for-production", + JWT_ACCESS_TOKEN_EXPIRE_MINUTES=15, + JWT_REFRESH_TOKEN_EXPIRE_DAYS=1, + POSTGRES_DSN="postgresql+asyncpg://postgres:postgres@localhost:5432/auth_test", + REDIS_DSN="redis://localhost:6379/1", + KAFKA_BOOTSTRAP_SERVERS="localhost:9092", + BCRYPT_ROUNDS=4, # fast rounds for tests + ) + + +# DOMAIN OBJECT FACTORIES +@pytest.fixture +def make_user(): + """Factory fixture — call it to get a ready-to-use User domain object.""" + + def _factory( + email: str = "user@example.com", + password_hash: str = "$2b$04$testhashvalue", + role: UserRole = UserRole.USER, + status: UserStatus = UserStatus.ACTIVE, + first_name: str | None = "Jane", + last_name: str | None = "Doe", + ) -> User: + user = User( + id=uuid4(), + email=email, + hashed_password=password_hash, + role=role, + status=status, + first_name=first_name, + last_name=last_name, + ) + return user + + return _factory + + +@pytest.fixture +def active_user(make_user) -> User: + return make_user(status=UserStatus.ACTIVE) + + +@pytest.fixture +def inactive_user(make_user) -> User: + return make_user(status=UserStatus.INACTIVE) + + +@pytest.fixture +def admin_user(make_user) -> User: + return make_user(role=UserRole.ADMIN, status=UserStatus.ACTIVE) + + +@pytest.fixture +def make_refresh_token(active_user): + """Factory for RefreshToken domain objects.""" + + def _factory( + user: User | None = None, + token: str = "some.refresh.token", + revoked: bool = False, + expires_in_days: int = 7, + ) -> RefreshToken: + rt = RefreshToken( + token=token, + user_id=(user or active_user).id, + expires_at=datetime.now(tz=timezone.utc) + timedelta(days=expires_in_days), + revoked=revoked, + ) + return rt + + return _factory + + +@pytest.fixture +def valid_refresh_token(make_refresh_token) -> RefreshToken: + return make_refresh_token() + + +@pytest.fixture +def expired_refresh_token(make_refresh_token) -> RefreshToken: + return make_refresh_token(expires_in_days=-1) + + +@pytest.fixture +def revoked_refresh_token(make_refresh_token) -> RefreshToken: + return make_refresh_token(revoked=True) + + +# REQUEST DTO FIXTURES +@pytest.fixture +def register_request() -> RegisterRequest: + return RegisterRequest( + email="newuser@example.com", + password="Str0ng!Pass", + first_name="New", + last_name="User", + ) + + +@pytest.fixture +def login_request() -> LoginRequest: + return LoginRequest(email="user@example.com", password="Str0ng!Pass") + + +@pytest.fixture +def refresh_request(valid_refresh_token) -> RefreshRequest: + return RefreshRequest(refresh_token=valid_refresh_token.token) + + +# INFRASTRUCTURE MOCKS (UNIT TESTS) +@pytest.fixture +def mock_user_repo(): + repo = AsyncMock() + repo.get_by_email.return_value = None + repo.get_by_id.return_value = None + repo.save.return_value = None + return repo + + +@pytest.fixture +def mock_token_repo(): + repo = AsyncMock() + repo.get_by_token.return_value = None + repo.save.return_value = None + repo.revoke_all_for_user.return_value = None + return repo + + +@pytest.fixture +def mock_redis(): + client = AsyncMock() + client.setex.return_value = True + client.get.return_value = None + return client + + +@pytest.fixture +def mock_kafka_producer(): + producer = AsyncMock() + producer.produce.return_value = None + return producer + + +# AUTHSERVICE (UNIT TESTS — ALL DEPS ARE MOCKS) +@pytest.fixture +def auth_service(mock_user_repo, mock_token_repo, mock_redis, mock_kafka_producer): + from src.services.auth import AuthService + + return AuthService( + user_repo=mock_user_repo, + token_repo=mock_token_repo, + redis_client=mock_redis, + kafka_producer=mock_kafka_producer, + ) + + +# FASTAPI TEST CLIENT (INTEGRATION TESTS) +@pytest_asyncio.fixture(scope="session") +async def test_app(test_settings): + """ + Full FastAPI app wired to real infrastructure. + Requires running Postgres, Redis, and Kafka (e.g. via docker-compose). + """ + from unittest.mock import patch + + with patch("src.core.config.get_settings", return_value=test_settings): + from src.main import create_app + application = create_app() + + async with application.router.lifespan_context(application): + yield application + + +@pytest_asyncio.fixture +async def client(test_app) -> AsyncIterator: + from httpx import AsyncClient, ASGITransport + + async with AsyncClient( + transport=ASGITransport(app=test_app), + base_url="http://testserver", + ) as ac: + yield ac diff --git a/backend/service/auth-service/tests/integration.py b/backend/service/auth-service/tests/integration.py new file mode 100644 index 0000000..def8bb9 --- /dev/null +++ b/backend/service/auth-service/tests/integration.py @@ -0,0 +1,288 @@ +""" +Integration tests — exercise the full HTTP stack against real infrastructure. +Prerequisites (e.g. via docker-compose): + - PostgreSQL on localhost:5432 (db: auth_test) ЭТИ ХОСТЫ НЕ Я ПРИДУМЫВАЛ + - Redis on localhost:6379 (db: 1) ЭТИ ХОСТЫ НЕ Я ПРИДУМЫВАЛ + - Kafka on localhost:9092 ЭТИ ХОСТЫ НЕ Я ПРИДУМЫВАЛ +Run only integration tests: + pytest tests/integration.py -m integration +All tests are marked with @pytest.mark.integration so they can be excluded +from fast CI runs with: pytest -m "not integration" +""" + +from __future__ import annotations + +import pytest +from httpx import AsyncClient + +pytestmark = pytest.mark.integration + + +# HELPERS +_BASE = "/api/v1" + +_VALID_USER = { + "email": "integration@example.com", + "password": "Integr@tion1", + "firstName": "Integration", + "lastName": "Test", +} + +_OTHER_USER = { + "email": "other@example.com", + "password": "0th3rUs3r!", +} + + +async def _register(client: AsyncClient, payload: dict | None = None) -> dict: + resp = await client.post(f"{_BASE}/auth/register", json=payload or _VALID_USER) + assert resp.status_code == 201, resp.text + return resp.json() + + +async def _login(client: AsyncClient, payload: dict | None = None) -> dict: + body = payload or {"email": _VALID_USER["email"], "password": _VALID_USER["password"]} + resp = await client.post(f"{_BASE}/auth/login", json=body) + assert resp.status_code == 200, resp.text + return resp.json() + + +# HEALTH +@pytest.mark.asyncio +async def test_health_endpoint(client: AsyncClient): + resp = await client.get("/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "ok" + assert "version" in body + + +# POST /auth/register +@pytest.mark.asyncio +class TestRegister: + async def test_register_success(self, client: AsyncClient): + resp = await client.post(f"{_BASE}/auth/register", json=_VALID_USER) + assert resp.status_code == 201 + body = resp.json() + assert body["user"]["email"] == _VALID_USER["email"] + assert "accessToken" in body["tokens"] + assert "refreshToken" in body["tokens"] + + async def test_register_duplicate_email_returns_409(self, client: AsyncClient): + await _register(client) + resp = await client.post(f"{_BASE}/auth/register", json=_VALID_USER) + assert resp.status_code == 409 + + async def test_register_invalid_email_returns_422(self, client: AsyncClient): + payload = {**_VALID_USER, "email": "not-an-email"} + resp = await client.post(f"{_BASE}/auth/register", json=payload) + assert resp.status_code == 422 + + async def test_register_weak_password_returns_422(self, client: AsyncClient): + payload = {**_VALID_USER, "email": "weak@example.com", "password": "weak"} + resp = await client.post(f"{_BASE}/auth/register", json=payload) + assert resp.status_code == 422 + + async def test_register_missing_email_returns_422(self, client: AsyncClient): + payload = {"password": "Str0ng!Pass"} + resp = await client.post(f"{_BASE}/auth/register", json=payload) + assert resp.status_code == 422 + + async def test_register_missing_password_returns_422(self, client: AsyncClient): + payload = {"email": "nopass@example.com"} + resp = await client.post(f"{_BASE}/auth/register", json=payload) + assert resp.status_code == 422 + + async def test_register_response_has_no_password_field(self, client: AsyncClient): + body = await _register(client, {**_VALID_USER, "email": "safe@example.com"}) + user_keys = body["user"].keys() + assert "password" not in user_keys + assert "hashedPassword" not in user_keys + + async def test_register_token_type_is_bearer(self, client: AsyncClient): + body = await _register(client, {**_VALID_USER, "email": "bearer@example.com"}) + assert body["tokens"]["tokenType"] == "bearer" + + +# POST /auth/login +@pytest.mark.asyncio +class TestLogin: + async def test_login_success(self, client: AsyncClient): + await _register(client) + resp = await client.post( + f"{_BASE}/auth/login", + json={"email": _VALID_USER["email"], "password": _VALID_USER["password"]}, + ) + assert resp.status_code == 200 + body = resp.json() + assert "accessToken" in body + assert "refreshToken" in body + + async def test_login_wrong_password_returns_401(self, client: AsyncClient): + await _register(client) + resp = await client.post( + f"{_BASE}/auth/login", + json={"email": _VALID_USER["email"], "password": "Wr0ng!Pass"}, + ) + assert resp.status_code == 401 + + async def test_login_unknown_email_returns_401(self, client: AsyncClient): + resp = await client.post( + f"{_BASE}/auth/login", + json={"email": "ghost@example.com", "password": "Str0ng!Pass"}, + ) + assert resp.status_code == 401 + + async def test_login_returns_expires_in(self, client: AsyncClient): + await _register(client) + body = await _login(client) + assert isinstance(body["expiresIn"], int) + assert body["expiresIn"] > 0 + + async def test_login_missing_fields_returns_422(self, client: AsyncClient): + resp = await client.post(f"{_BASE}/auth/login", json={"email": "x@x.com"}) + assert resp.status_code == 422 + + +# POST /auth/refresh +@pytest.mark.asyncio +class TestRefresh: + async def test_refresh_returns_new_access_token(self, client: AsyncClient): + await _register(client) + tokens = await _login(client) + + resp = await client.post( + f"{_BASE}/auth/refresh", + json={"refreshToken": tokens["refreshToken"]}, + ) + assert resp.status_code == 200 + body = resp.json() + assert "accessToken" in body + # New access token must differ from the original + assert body["accessToken"] != tokens["accessToken"] + + async def test_refresh_token_rotation(self, client: AsyncClient): + """Using the same refresh token twice must fail on the second attempt.""" + await _register(client, {**_VALID_USER, "email": "rotation@example.com"}) + tokens = await _login(client, {"email": "rotation@example.com", "password": _VALID_USER["password"]}) + refresh_token = tokens["refreshToken"] + + # First use — valid + resp1 = await client.post( + f"{_BASE}/auth/refresh", json={"refreshToken": refresh_token} + ) + assert resp1.status_code == 200 + + # Second use — must be rejected (token was rotated) + resp2 = await client.post( + f"{_BASE}/auth/refresh", json={"refreshToken": refresh_token} + ) + assert resp2.status_code == 401 + + async def test_refresh_with_invalid_token_returns_401(self, client: AsyncClient): + resp = await client.post( + f"{_BASE}/auth/refresh", json={"refreshToken": "not.a.real.token"} + ) + assert resp.status_code == 401 + + async def test_refresh_with_access_token_returns_401(self, client: AsyncClient): + await _register(client) + tokens = await _login(client) + # Pass the access token where a refresh token is expected + resp = await client.post( + f"{_BASE}/auth/refresh", json={"refreshToken": tokens["accessToken"]} + ) + assert resp.status_code == 401 + + async def test_refresh_missing_token_returns_422(self, client: AsyncClient): + resp = await client.post(f"{_BASE}/auth/refresh", json={}) + assert resp.status_code == 422 + + +# POST /auth/logout +@pytest.mark.asyncio +class TestLogout: + async def test_logout_success(self, client: AsyncClient): + await _register(client) + tokens = await _login(client) + + resp = await client.post( + f"{_BASE}/auth/logout", + json={"refreshToken": tokens["refreshToken"]}, + ) + assert resp.status_code == 204 + + async def test_logout_invalidates_refresh_token(self, client: AsyncClient): + await _register(client, {**_VALID_USER, "email": "logout@example.com"}) + tokens = await _login(client, {"email": "logout@example.com", "password": _VALID_USER["password"]}) + refresh_token = tokens["refreshToken"] + + await client.post(f"{_BASE}/auth/logout", json={"refreshToken": refresh_token}) + + # Subsequent refresh with the now-revoked token must fail + resp = await client.post( + f"{_BASE}/auth/refresh", json={"refreshToken": refresh_token} + ) + assert resp.status_code == 401 + + async def test_logout_unknown_token_still_returns_204(self, client: AsyncClient): + """Logout is idempotent — unknown tokens are silently accepted.""" + resp = await client.post( + f"{_BASE}/auth/logout", json={"refreshToken": "unknown.token.value"} + ) + assert resp.status_code == 204 + + +# GET /auth/me +@pytest.mark.asyncio +class TestProtectedRoute: + async def test_me_returns_current_user(self, client: AsyncClient): + await _register(client) + tokens = await _login(client) + + resp = await client.get( + f"{_BASE}/auth/me", + headers={"Authorization": f"Bearer {tokens['accessToken']}"}, + ) + assert resp.status_code == 200 + assert resp.json()["email"] == _VALID_USER["email"] + + async def test_me_without_token_returns_401(self, client: AsyncClient): + resp = await client.get(f"{_BASE}/auth/me") + assert resp.status_code == 401 + + async def test_me_with_expired_token_returns_401(self, client: AsyncClient): + from datetime import timedelta + from jose import jwt as jose_jwt + from src.core.config import get_settings + + settings = get_settings() + past = __import__("datetime").datetime.now(tz=__import__("datetime").timezone.utc) - timedelta(seconds=1) + expired = jose_jwt.encode( + {"sub": str(__import__("uuid").uuid4()), "type": "access", "exp": past}, + settings.JWT_SECRET_KEY, + algorithm=settings.JWT_ALGORITHM, + ) + resp = await client.get( + f"{_BASE}/auth/me", + headers={"Authorization": f"Bearer {expired}"}, + ) + assert resp.status_code == 401 + + async def test_me_with_refresh_token_returns_401(self, client: AsyncClient): + """A refresh JWT must be rejected on an access-token-protected route.""" + await _register(client) + tokens = await _login(client) + + resp = await client.get( + f"{_BASE}/auth/me", + headers={"Authorization": f"Bearer {tokens['refreshToken']}"}, + ) + assert resp.status_code == 401 + + async def test_me_with_malformed_header_returns_401(self, client: AsyncClient): + resp = await client.get( + f"{_BASE}/auth/me", + headers={"Authorization": "NotBearer token"}, + ) + assert resp.status_code == 401 \ No newline at end of file diff --git a/backend/service/auth-service/tests/unit.py b/backend/service/auth-service/tests/unit.py new file mode 100644 index 0000000..68c43e9 --- /dev/null +++ b/backend/service/auth-service/tests/unit.py @@ -0,0 +1,544 @@ +""" +Unit tests — no real I/O, all infrastructure is mocked. +Coverage targets: + - src/core/security.py : JWT creation / decoding, password hashing + - src/core/exceptions.py : exception hierarchy and attributes + - src/domain/entities.py : User and RefreshToken domain logic + - src/services/validators.py: password strength and email-domain rules + - src/services/auth.py : AuthService flows (register, login, refresh, logout) +""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from unittest.mock import AsyncMock, patch +from uuid import uuid4 + +import pytest + +from src.core.exceptions import ( + InvalidCredentialsError, + InvalidTokenError, + TokenExpiredError, + UserAlreadyExistsError, + UserInactiveError, + WeakPasswordError, +) +from src.core.security import ( + create_access_token, + create_refresh_token, + decode_refresh_token, + decode_token, + hash_password, + verify_password, +) +from src.domain.entities import RefreshToken, User, UserRole, UserStatus +from src.schemes.dto import LoginRequest, RefreshRequest, RegisterRequest +from src.services.validators import validate_email_domain, validate_password_strength + + +# core/security.py +class TestPasswordHashing: + def test_hash_returns_string(self): + assert isinstance(hash_password("Str0ng!Pass"), str) + + def test_hash_is_not_plaintext(self): + assert hash_password("Str0ng!Pass") != "Str0ng!Pass" + + def test_two_hashes_differ(self): + # bcrypt salts must differ between calls + assert hash_password("Str0ng!Pass") != hash_password("Str0ng!Pass") + + def test_verify_correct_password(self): + hashed = hash_password("Str0ng!Pass") + assert verify_password("Str0ng!Pass", hashed) is True + + def test_verify_wrong_password(self): + hashed = hash_password("Str0ng!Pass") + assert verify_password("WrongPass1!", hashed) is False + + def test_verify_empty_string_fails(self): + hashed = hash_password("Str0ng!Pass") + assert verify_password("", hashed) is False + + +class TestJWT: + def test_access_token_is_string(self): + token = create_access_token(uuid4()) + assert isinstance(token, str) + assert len(token.split(".")) == 3 # header.payload.signature + + def test_refresh_token_is_string(self): + token = create_refresh_token(uuid4()) + assert isinstance(token, str) + + def test_decode_access_token_returns_subject(self): + user_id = uuid4() + token = create_access_token(user_id) + payload = decode_token(token) + assert payload["sub"] == str(user_id) + + def test_decode_access_token_type(self): + token = create_access_token(uuid4()) + payload = decode_token(token) + assert payload["type"] == "access" + + def test_decode_refresh_token_type(self): + token = create_refresh_token(uuid4()) + payload = decode_refresh_token(token) + assert payload["type"] == "refresh" + + def test_extra_claims_are_included(self): + token = create_access_token(uuid4(), extra_claims={"role": "admin"}) + payload = decode_token(token) + assert payload["role"] == "admin" + + def test_access_token_rejected_as_refresh(self): + token = create_access_token(uuid4()) + with pytest.raises(InvalidTokenError): + decode_refresh_token(token) + + def test_refresh_token_rejected_as_access(self): + token = create_refresh_token(uuid4()) + with pytest.raises(InvalidTokenError): + decode_token(token) + + def test_tampered_token_raises_invalid(self): + token = create_access_token(uuid4()) + tampered = token[:-4] + "xxxx" + with pytest.raises(InvalidTokenError): + decode_token(tampered) + + def test_expired_token_raises_expired(self): + from jose import jwt as jose_jwt + from src.core.config import get_settings + settings = get_settings() + + past = datetime.now(tz=timezone.utc) - timedelta(seconds=1) + payload = {"sub": str(uuid4()), "type": "access", "exp": past} + expired_token = jose_jwt.encode( + payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM + ) + with pytest.raises(TokenExpiredError): + decode_token(expired_token) + + def test_garbage_string_raises_invalid(self): + with pytest.raises(InvalidTokenError): + decode_token("not.a.token") + + +# core/exceptions.py +class TestExceptions: + def test_default_detail_used_when_none_given(self): + exc = InvalidCredentialsError() + assert exc.detail == "Invalid email or password." + + def test_custom_detail_overrides_default(self): + exc = InvalidTokenError("Custom message.") + assert exc.detail == "Custom message." + + def test_exception_is_str_representable(self): + exc = UserAlreadyExistsError() + assert str(exc) == exc.detail + + def test_status_codes(self): + from http import HTTPStatus + assert InvalidCredentialsError.status_code == HTTPStatus.UNAUTHORIZED + assert UserAlreadyExistsError.status_code == HTTPStatus.CONFLICT + assert UserInactiveError.status_code == HTTPStatus.FORBIDDEN + assert WeakPasswordError.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +# domain/entities.py — User +class TestUserEntity: + def test_default_status_is_pending(self): + user = User(email="a@b.com", hashed_password="hash") + assert user.status == UserStatus.PENDING_VERIFICATION + + def test_activate_sets_active(self): + user = User(email="a@b.com", hashed_password="hash") + user.activate() + assert user.is_active is True + assert user.status == UserStatus.ACTIVE + + def test_deactivate_sets_inactive(self): + user = User(email="a@b.com", hashed_password="hash") + user.activate() + user.deactivate() + assert user.is_active is False + assert user.status == UserStatus.INACTIVE + + def test_ban_sets_banned(self): + user = User(email="a@b.com", hashed_password="hash") + user.ban() + assert user.status == UserStatus.BANNED + assert user.is_active is False + + def test_activate_updates_updated_at(self): + user = User(email="a@b.com", hashed_password="hash") + before = user.updated_at + user.activate() + assert user.updated_at >= before + + def test_is_admin_true_for_admin_role(self): + user = User(email="a@b.com", hashed_password="hash", role=UserRole.ADMIN) + assert user.is_admin is True + + def test_is_admin_false_for_user_role(self): + user = User(email="a@b.com", hashed_password="hash", role=UserRole.USER) + assert user.is_admin is False + + def test_full_name_both_parts(self): + user = User( + email="a@b.com", hashed_password="h", + first_name="Jane", last_name="Doe", + ) + assert user.full_name == "Jane Doe" + + def test_full_name_only_first(self): + user = User(email="a@b.com", hashed_password="h", first_name="Jane") + assert user.full_name == "Jane" + + def test_full_name_none_when_empty(self): + user = User(email="a@b.com", hashed_password="h") + assert user.full_name is None + + def test_unique_ids_by_default(self): + u1 = User(email="a@b.com", hashed_password="h") + u2 = User(email="b@b.com", hashed_password="h") + assert u1.id != u2.id + + +# domain/entities.py — RefreshToken +class TestRefreshTokenEntity: + def _make(self, *, days: int = 7, revoked: bool = False) -> RefreshToken: + return RefreshToken( + token="tok", + user_id=uuid4(), + expires_at=datetime.now(tz=timezone.utc) + timedelta(days=days), + revoked=revoked, + ) + + def test_valid_token_is_valid(self): + assert self._make().is_valid is True + + def test_expired_token_is_not_valid(self): + assert self._make(days=-1).is_valid is False + + def test_revoked_token_is_not_valid(self): + assert self._make(revoked=True).is_valid is False + + def test_revoke_sets_flag(self): + rt = self._make() + rt.revoke() + assert rt.revoked is True + assert rt.is_valid is False + + def test_is_expired_false_for_future(self): + assert self._make(days=1).is_expired is False + + def test_is_expired_true_for_past(self): + assert self._make(days=-1).is_expired is True + + +# services/validators.py +class TestPasswordValidator: + @pytest.mark.parametrize("pw", [ + "Str0ng!Pass", + "C0mpl3x@Word", + "V3ryS3cur3#", + "A1b2C3d4!", + ]) + def test_valid_passwords_pass(self, pw: str): + validate_password_strength(pw) # should not raise + + @pytest.mark.parametrize("pw,reason", [ + ("short1A!", "too short"), + ("alllowercase1!", "no uppercase"), + ("ALLUPPERCASE1!", "no lowercase"), + ("NoDigitsHere!", "no digit"), + ("NoSpecial1Char", "no special char"), + ("Password1!", "common password"), + ]) + def test_invalid_passwords_raise(self, pw: str, reason: str): + with pytest.raises(WeakPasswordError, match=""), \ + pytest.raises(WeakPasswordError): + validate_password_strength(pw) + + def test_common_password_specific_message(self): + with pytest.raises(WeakPasswordError, match="too common"): + validate_password_strength("Password1!") + + def test_password_at_max_length_passes(self): + # 128 chars, meets all rules + pw = "Aa1!" + "x" * 124 + validate_password_strength(pw) + + def test_password_over_max_length_fails(self): + pw = "Aa1!" + "x" * 125 # 129 chars + with pytest.raises(WeakPasswordError): + validate_password_strength(pw) + + +class TestEmailDomainValidator: + def test_no_blocked_domains_passes(self): + validate_email_domain("user@any.com", blocked_domains=None) + + def test_allowed_domain_passes(self): + validate_email_domain("user@gmail.com", blocked_domains={"tempmail.com"}) + + def test_blocked_domain_raises(self): + from src.core.exceptions import ValidationException + with pytest.raises(ValidationException): + validate_email_domain("user@tempmail.com", blocked_domains={"tempmail.com"}) + + def test_domain_check_is_case_insensitive(self): + from src.core.exceptions import ValidationException + with pytest.raises(ValidationException): + validate_email_domain("user@TempMail.COM", blocked_domains={"tempmail.com"}) + + +# services/auth.py — AuthService +@pytest.mark.asyncio +class TestAuthServiceRegister: + async def test_register_returns_user_and_tokens( + self, auth_service, register_request, mock_user_repo + ): + mock_user_repo.get_by_email.return_value = None + result = await auth_service.register(register_request) + + assert result.user.email == register_request.email + assert result.tokens.access_token + assert result.tokens.refresh_token + + async def test_register_saves_user( + self, auth_service, register_request, mock_user_repo + ): + mock_user_repo.get_by_email.return_value = None + await auth_service.register(register_request) + mock_user_repo.save.assert_awaited_once() + + async def test_register_persists_refresh_token( + self, auth_service, register_request, mock_token_repo + ): + await auth_service.register(register_request) + mock_token_repo.save.assert_awaited() + + async def test_register_publishes_kafka_event( + self, auth_service, register_request, mock_kafka_producer + ): + await auth_service.register(register_request) + mock_kafka_producer.produce.assert_awaited_once() + + async def test_register_raises_if_email_taken( + self, auth_service, register_request, mock_user_repo, active_user + ): + mock_user_repo.get_by_email.return_value = active_user + with pytest.raises(UserAlreadyExistsError): + await auth_service.register(register_request) + + async def test_register_raises_on_weak_password( + self, auth_service, mock_user_repo + ): + mock_user_repo.get_by_email.return_value = None + weak_req = RegisterRequest(email="x@example.com", password="weakpass") + with pytest.raises(WeakPasswordError): + await auth_service.register(weak_req) + + async def test_register_stores_hashed_password( + self, auth_service, register_request, mock_user_repo + ): + mock_user_repo.get_by_email.return_value = None + await auth_service.register(register_request) + + saved_user: User = mock_user_repo.save.call_args[0][0] + assert saved_user.hashed_password != register_request.password + + async def test_register_activates_user( + self, auth_service, register_request, mock_user_repo + ): + mock_user_repo.get_by_email.return_value = None + result = await auth_service.register(register_request) + assert result.user.status == UserStatus.ACTIVE + + +@pytest.mark.asyncio +class TestAuthServiceLogin: + async def test_login_returns_token_pair( + self, auth_service, login_request, active_user, mock_user_repo + ): + active_user.hashed_password = hash_password(login_request.password) + mock_user_repo.get_by_email.return_value = active_user + + result = await auth_service.login(login_request) + assert result.access_token + assert result.refresh_token + + async def test_login_wrong_password_raises( + self, auth_service, login_request, active_user, mock_user_repo + ): + active_user.hashed_password = hash_password("DifferentP@ss1") + mock_user_repo.get_by_email.return_value = active_user + + with pytest.raises(InvalidCredentialsError): + await auth_service.login(login_request) + + async def test_login_unknown_email_raises( + self, auth_service, login_request, mock_user_repo + ): + mock_user_repo.get_by_email.return_value = None + with pytest.raises(InvalidCredentialsError): + await auth_service.login(login_request) + + async def test_login_inactive_user_raises( + self, auth_service, login_request, inactive_user, mock_user_repo + ): + inactive_user.hashed_password = hash_password(login_request.password) + mock_user_repo.get_by_email.return_value = inactive_user + + with pytest.raises(UserInactiveError): + await auth_service.login(login_request) + + async def test_login_persists_refresh_token( + self, auth_service, login_request, active_user, mock_user_repo, mock_token_repo + ): + active_user.hashed_password = hash_password(login_request.password) + mock_user_repo.get_by_email.return_value = active_user + + await auth_service.login(login_request) + mock_token_repo.save.assert_awaited() + + +@pytest.mark.asyncio +class TestAuthServiceRefresh: + async def test_refresh_returns_new_access_token( + self, + auth_service, + active_user, + valid_refresh_token, + mock_user_repo, + mock_token_repo, + ): + real_refresh = create_refresh_token(active_user.id) + valid_refresh_token.token = real_refresh + + mock_token_repo.get_by_token.return_value = valid_refresh_token + mock_user_repo.get_by_id.return_value = active_user + + result = await auth_service.refresh(RefreshRequest(refresh_token=real_refresh)) + assert result.access_token + + async def test_refresh_revokes_old_token( + self, + auth_service, + active_user, + valid_refresh_token, + mock_user_repo, + mock_token_repo, + ): + real_refresh = create_refresh_token(active_user.id) + valid_refresh_token.token = real_refresh + + mock_token_repo.get_by_token.return_value = valid_refresh_token + mock_user_repo.get_by_id.return_value = active_user + + await auth_service.refresh(RefreshRequest(refresh_token=real_refresh)) + assert valid_refresh_token.revoked is True + + async def test_refresh_blacklists_old_token_in_redis( + self, + auth_service, + active_user, + valid_refresh_token, + mock_user_repo, + mock_token_repo, + mock_redis, + ): + real_refresh = create_refresh_token(active_user.id) + valid_refresh_token.token = real_refresh + + mock_token_repo.get_by_token.return_value = valid_refresh_token + mock_user_repo.get_by_id.return_value = active_user + + await auth_service.refresh(RefreshRequest(refresh_token=real_refresh)) + mock_redis.setex.assert_awaited_once() + key = mock_redis.setex.call_args[0][0] + assert key.startswith("blacklist:") + + async def test_refresh_unknown_token_raises( + self, auth_service, active_user, mock_token_repo + ): + real_refresh = create_refresh_token(active_user.id) + mock_token_repo.get_by_token.return_value = None + + with pytest.raises(InvalidTokenError): + await auth_service.refresh(RefreshRequest(refresh_token=real_refresh)) + + async def test_refresh_revoked_token_raises_and_revokes_all( + self, + auth_service, + active_user, + revoked_refresh_token, + mock_user_repo, + mock_token_repo, + ): + real_refresh = create_refresh_token(active_user.id) + revoked_refresh_token.token = real_refresh + + mock_token_repo.get_by_token.return_value = revoked_refresh_token + + with pytest.raises(InvalidTokenError): + await auth_service.refresh(RefreshRequest(refresh_token=real_refresh)) + + mock_token_repo.revoke_all_for_user.assert_awaited_once_with(active_user.id) + + async def test_refresh_inactive_user_raises( + self, + auth_service, + inactive_user, + valid_refresh_token, + mock_user_repo, + mock_token_repo, + ): + real_refresh = create_refresh_token(inactive_user.id) + valid_refresh_token.token = real_refresh + + mock_token_repo.get_by_token.return_value = valid_refresh_token + mock_user_repo.get_by_id.return_value = inactive_user + + with pytest.raises(UserInactiveError): + await auth_service.refresh(RefreshRequest(refresh_token=real_refresh)) + + async def test_refresh_with_garbage_jwt_raises(self, auth_service): + with pytest.raises(InvalidTokenError): + await auth_service.refresh(RefreshRequest(refresh_token="garbage")) + + +@pytest.mark.asyncio +class TestAuthServiceLogout: + async def test_logout_revokes_stored_token( + self, auth_service, valid_refresh_token, mock_token_repo + ): + mock_token_repo.get_by_token.return_value = valid_refresh_token + await auth_service.logout(valid_refresh_token.token) + assert valid_refresh_token.revoked is True + + async def test_logout_blacklists_token( + self, auth_service, valid_refresh_token, mock_token_repo, mock_redis + ): + mock_token_repo.get_by_token.return_value = valid_refresh_token + await auth_service.logout(valid_refresh_token.token) + mock_redis.setex.assert_awaited_once() + + async def test_logout_unknown_token_does_not_raise( + self, auth_service, mock_token_repo, mock_redis + ): + mock_token_repo.get_by_token.return_value = None + await auth_service.logout("unknown-token") # should complete silently + mock_redis.setex.assert_awaited_once() + + async def test_logout_all_delegates_to_repo( + self, auth_service, active_user, mock_token_repo + ): + await auth_service.logout_all(active_user.id) + mock_token_repo.revoke_all_for_user.assert_awaited_once_with(active_user.id) \ No newline at end of file diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index cd9b299..0000000 --- a/docs/API.md +++ /dev/null @@ -1,749 +0,0 @@ -# Структура и Спецификация API - -![title_img](../img/api-md.png) - ->[!IMPORTANT] -> Данная спецификация является единым источником истины (SSOT) для взаимодействия с API платформы STREMO. Документ разбит по микросервисам. Для каждой ручки приведены полные примеры JSON-ответов для всех возможных статусов (успешных и ошибок). - ->[!NOTE] -> **Базовый URL:** `https://api.stremo.com/v1` -> Все закрытые эндпоинты требуют заголовок: `Authorization: Bearer ` - ---- - -## **Auth-service** - -> Данный сервис отвечает за безопасность платформы. Он управляет регистрацией, выдачей и ротацией JWT-токенов, сессиями пользователей, а также интеграциями со сторонними провайдерами (**OAuth 2.0**) и двухфакторной аутентификацией (**Mobile ID**). - -#### *Классическая регистрация* `POST /auth/register` - -Создает новую учетную запись пользователя. - -**Request Body** - -| Поле | Тип | Обяз. | Описание и Валидация | -| :--- | :--- | :--- | :--- | -| `email` | `string` | Да | Контактный email. Формат: валидный email (RFC 5322). Макс. 255 символов. | -| `username` | `string` | Да | Уникальный никнейм. Только a-z, 0-9 и `_`. Длина: от 3 до 25 символов. | -| `password` | `string` | Да | Мин. 8 символов, обязательно 1 заглавная буква, 1 строчная, 1 цифра, 1 спецсимвол. | - -Примеры ответов - ->[!TIP] -> **201 CREATED (OK)** -> ```json -> { -> "id": "usr-550e8400-e29b-41d4-a716-446655440000", -> "message": "User successfully registered. Please check your email for verification." -> } -> ``` - ->[!CAUTION] -> **400 Bad Request** -> ```json -> { -> "error": { -> "code": "VALIDATION_FAILED", -> "message": "Введенные данные не соответствуют требованиям", -> "details": [ -> { -> "field": "password", -> "issue": "Пароль должен содержать как минимум один спецсимвол" -> } -> ], -> "request_id": "req-99f5a-4b12" -> } -> } -> ``` -> -> **409 Conflict** -> ```json -> { -> "error": { -> "code": "USER_ALREADY_EXISTS", -> "message": "Пользователь с такими данными уже существует", -> "details": [ -> { -> "field": "username", -> "issue": "Никнейм streamer_pro уже занят" -> } -> ], -> "request_id": "req-99f5b-5c23" -> } -> } -> ``` - -#### *Авторизация* `POST /auth/login` - -Проверяет учетные данные и возвращает пару токенов для доступа к API. - -**Request Body** - -| Поле | Тип | Обяз. | Описание | -| :--- | :--- | :--- | :--- | -| `email` | `string` | Да | Email пользователя. | -| `password` | `string` | Да | Пароль в открытом виде. | - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI...", // Короткоживущий токен для запросов -> "refresh_token": "def50200508a8a4f8d5...", // Долгоживущий токен для обновления сессии -> "expires_in": 900, // Время жизни access_token в секундах (15 минут) -> "token_type": "Bearer" -> } -> ``` - ->[!CAUTION] -> **401 Unauthorized** -> ```json -> { -> "error": { -> "code": "INVALID_CREDENTIALS", -> "message": "Неверный email или пароль", -> "request_id": "req-11a2b-3c44" -> } -> } -> ``` -> -> **403 Forbidden** -> ```json -> { -> "error": { -> "code": "ACCOUNT_BANNED", -> "message": "Ваш аккаунт был перманентно заблокирован", -> "request_id": "req-11a2b-3c45" -> } -> } -> ``` -> -> **429 Too Many Requests** -> ```json -> { -> "error": { -> "code": "RATE_LIMIT_EXCEEDED", -> "message": "Слишком много неудачных попыток входа. Попробуйте через 15 минут.", -> "request_id": "req-11a2b-3c46" -> } -> } -> ``` - -#### *Инициализация Mobile ID (Беспарольный вход / 2FA)* `POST /auth/mobile-id/init` - -Отправляет Push-уведомление на телефон пользователя через провайдера связи. - -**Request Body** - -| Поле | Тип | Обяз. | Описание и Валидация | -| :--- | :--- | :--- | :--- | -| `phone_number` | `string` | Да | Номер телефона в формате E.164 (например, `+12345678900`). | - -Примеры ответов - ->[!TIP] -> **202 Accepted** -> ```json -> { -> "session_id": "mob-999888", // Сохраните этот ID для проверки статуса авторизации -> "control_code": "4512", // Покажите этот код пользователю на экране! -> "expires_in": 120 // Сессия сгорит через 120 секунд -> } -> ``` - ->[!CAUTION] -> **404 Not Found** -> ```json -> { -> "error": { -> "code": "PHONE_NOT_REGISTERED", -> "message": "Данный номер телефона не привязан ни к одному аккаунту", -> "request_id": "req-22b3c-4d55" -> } -> } -> ``` - ---- - -## **User-profile-service** - -> Микросервис управляет публичной и приватной информацией о пользователях. - -#### *Получение профиля текущего пользователя* `GET /users/me` - -Возвращает полную информацию об авторизованном пользователе. Запрос требует JWT токен. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "id": "usr-123", -> "username": "streamer_pro", -> "email": "user@example.com", // Возвращается только владельцу -> "bio": "Professional CS2 player", -> "avatar_url": "https://cdn.stremo.com/avatars/usr-123.png", -> "followers_count": 1500, -> "is_verified": true // Галочка верификации канала -> } -> ``` - ->[!CAUTION] -> **401 Unauthorized** -> ```json -> { -> "error": { -> "code": "UNAUTHORIZED", -> "message": "Токен доступа не передан или истек", -> "request_id": "req-33c4d-5e66" -> } -> } -> ``` - -#### *Загрузка аватара* `POST /users/me/avatar` - -Загружает новое изображение профиля в S3 хранилище. `Content-Type: multipart/form-data`. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "avatar_url": "https://cdn.stremo.com/avatars/usr-123_new.png", -> "message": "Аватар успешно обновлен" -> } -> ``` - ->[!CAUTION] -> **400 Bad Request** -> ```json -> { -> "error": { -> "code": "UNSUPPORTED_FILE_TYPE", -> "message": "Разрешены только форматы JPEG, PNG и WebP", -> "request_id": "req-44d5e-6f77" -> } -> } -> ``` -> -> **413 Payload Too Large** -> ```json -> { -> "error": { -> "code": "FILE_TOO_LARGE", -> "message": "Размер файла превышает лимит в 5 МБ", -> "request_id": "req-44d5e-6f78" -> } -> } -> ``` - ---- - -## **Billing-service** - -> Микросервис обрабатывает транзакции с балансом (донат Bits, подписки). - -#### *Отправка доната (Bits) на стрим* `POST /billing/donate` - -Списывает внутреннюю валюту со счета зрителя и переводит стримеру. - -**Request Body** - -| Поле | Тип | Обяз. | Описание и Валидация | -| :--- | :--- | :--- | :--- | -| `channel_id` | `string` | Да | UUID стримера, которому предназначается донат. | -| `amount_bits` | `integer` | Да | Количество отправляемой валюты. Минимум: 10, Максимум: 500,000. | -| `message` | `string` | Нет | Сообщение для стримера. Макс. 255 символов. | - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "transaction_id": "txn-abc123def456", -> "remaining_balance": 4500, // Новый баланс зрителя после списания -> "message": "Донат успешно отправлен" -> } -> ``` - ->[!CAUTION] -> **400 Bad Request** -> ```json -> { -> "error": { -> "code": "AMOUNT_TOO_LOW", -> "message": "Минимальная сумма доната составляет 10 Bits", -> "request_id": "req-55e6f-7a88" -> } -> } -> ``` -> -> **402 Payment Required** -> ```json -> { -> "error": { -> "code": "INSUFFICIENT_FUNDS", -> "message": "На вашем балансе недостаточно Bits для совершения операции", -> "details": [ -> { -> "field": "amount_bits", -> "issue": "Требуется 500, доступно 150" -> } -> ], -> "request_id": "req-55e6f-7a89" -> } -> } -> ``` -> -> **404 Not Found** -> ```json -> { -> "error": { -> "code": "CHANNEL_NOT_FOUND", -> "message": "Стример с указанным ID не найден", -> "request_id": "req-55e6f-7a90" -> } -> } -> ``` - ---- - -## **Stream-meta-service** - -> Микросервис управляет метаданными эфира, категориями и каталогом трансляций. - -#### *Изменение метаданных трансляции* `PUT /streams/{stream_id}/meta` - -Обновляет карточку стрима. Рассылает WebSocket событие всем текущим зрителям. - -**Request Body** - -| Поле | Тип | Обяз. | Описание и Валидация | -| :--- | :--- | :--- | :--- | -| `title` | `string` | Да | Название стрима (от 1 до 140 символов). | -| `category_id` | `string` | Да | ID игры (например, `cs2`). | - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "stream_id": "str-987", -> "title": "Road to Global Elite | CS2", -> "category_id": "cs2", -> "updated_at": "2026-05-20T10:05:00Z" -> } -> ``` - ->[!CAUTION] -> **400 Bad Request** -> ```json -> { -> "error": { -> "code": "CATEGORY_NOT_FOUND", -> "message": "Указанная категория игры не существует в базе", -> "request_id": "req-66f7a-8b11" -> } -> } -> ``` -> -> **403 Forbidden** -> ```json -> { -> "error": { -> "code": "NOT_STREAM_OWNER", -> "message": "У вас нет прав на редактирование этой трансляции", -> "request_id": "req-66f7a-8b12" -> } -> } -> ``` - -#### *Список активных трансляций (Каталог)* `GET /streams/live` - -Возвращает список текущих эфиров. Поддерживает курсорную пагинацию. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "data": [ -> { -> "stream_id": "str-987", -> "user": { -> "id": "usr-123", -> "username": "streamer_pro", -> "avatar_url": "https://cdn.stremo.com/avatars/usr-123.png" -> }, -> "title": "Grand Final!", -> "category": {"id": "cs2", "name": "Counter-Strike 2"}, -> "viewers": 15405, // Текущий онлайн (CCV) обновляется раз в 15 секунд -> "thumbnail_url": "https://cdn.stremo.com/thumbs/str-987.jpg", // Скриншот потока (обновляется раз в 5 минут) -> "tags": ["esports", "ru"], -> "is_mature": false -> } -> ], -> "next_cursor": "YXNkcW..." // Использовать в следующем GET запросе для подгрузки при скролле вниз -> } -> ``` - ---- - -## **Chat-service** - -> Микросервис доставляет текстовые сообщения сотням тысяч зрителей в реальном времени. - -#### *Получение истории сообщений* `GET /chat/{channel_id}/history` - -Запрашивается клиентом сразу после подключения к WS для инициализации окна чата. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "messages": [ -> { -> "id": "msg-001", -> "author": "viewer1", -> "text": "Привет стример!", -> "badges": ["subscriber", "vip"], // Значки пользователя -> "timestamp": "2026-05-20T10:00:00Z" -> }, -> { -> "id": "msg-002", -> "author": "viewer2", -> "text": "GG WP", -> "badges": [], -> "timestamp": "2026-05-20T10:00:05Z" -> } -> ] -> } -> ``` - ->[!CAUTION] -> **404 Not Found** -> ```json -> { -> "error": { -> "code": "CHANNEL_NOT_FOUND", -> "message": "Чат для данного канала не найден", -> "request_id": "req-77a8b-9c22" -> } -> } -> ``` - ---- - -## **Moderation-service** - -> Обрабатывает выдачу банов, таймаутов и автоматическую проверку сообщений на спам. - -#### *Принудительный бан в чате* `POST /moderation/channels/{channel_id}/ban` - -Блокирует пользователя на канале и выкидывает его из WebSocket соединения. - -**Request Body** - -| Поле | Тип | Обяз. | Описание и Валидация | -| :--- | :--- | :--- | :--- | -| `user_id` | `string` | Да | ID пользователя-нарушителя. | -| `reason` | `string` | Нет | Причина бана (остается в логах для апелляции). | -| `delete_recent` | `boolean` | Нет | Если `true`, удаляет все сообщения пользователя за последние 10 минут. По умолчанию `true`. | - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "status": "banned", -> "target_user_id": "usr-badguy", -> "moderator_id": "usr-mod123", -> "message": "Пользователь забанен, его недавние сообщения удалены" -> } -> ``` - ->[!CAUTION] -> **403 Forbidden** -> ```json -> { -> "error": { -> "code": "MISSING_PERMISSIONS", -> "message": "У вас нет прав модератора для выполнения этого действия", -> "request_id": "req-88b9c-0d33" -> } -> } -> ``` -> -> **409 Conflict** -> ```json -> { -> "error": { -> "code": "ALREADY_BANNED", -> "message": "Этот пользователь уже забанен на данном канале", -> "request_id": "req-88b9c-0d34" -> } -> } -> ``` - ---- - -## **Analytics-service** - -> Считает статистику в реальном времени (CCV) и агрегирует метрики. - -#### *Статистика стрима* `GET /analytics/streams/{stream_id}/summary` - -Возвращает бизнес-показатели завершенной или текущей трансляции для стримера. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "stream_id": "str-987", -> "peak_viewers": 16500, // Пиковый онлайн -> "average_viewers": 12400, // Средний онлайн -> "new_followers": 340, // Сколько новых подписчиков пришло -> "revenue_bits": 45000, // Заработано Bits -> "revenue_fiat": 120.50 // Заработано фиата (с платных подписок) -> } -> ``` - ->[!CAUTION] -> **403 Forbidden** -> ```json -> { -> "error": { -> "code": "ACCESS_DENIED", -> "message": "Вы можете просматривать аналитику только своих трансляций", -> "request_id": "req-99c0d-1e44" -> } -> } -> ``` - ---- - -## **Notification-service** - -> Управляет колокольчиком (In-App уведомления). - -#### *Список уведомлений* `GET /notifications` - -Возвращает список непрочитанных уведомлений. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "data": [ -> { -> "id": "notif-001", -> "type": "stream_started", // Тип уведомления -> "title": "streamer_pro начал трансляцию!", -> "body": "Заходи смотреть Road to Global Elite", -> "action_url": "https://stremo.com/streamer_pro", -> "created_at": "2026-05-20T09:00:00Z", -> "is_read": false -> } -> ], -> "unread_count": 1 -> } -> ``` - ->[!CAUTION] -> **401 Unauthorized** -> ```json -> { -> "error": { -> "code": "UNAUTHORIZED", -> "message": "Требуется авторизация для просмотра уведомлений", -> "request_id": "req-00d1e-2f55" -> } -> } -> ``` - ---- - -## **Vod-manager-service** - -> Микросервис управляет доступом к сохраненным записям прошлых трансляций (Video on Demand) и клипам. - -#### *Список записей канала* `GET /vods/channel/{channel_id}` - -Возвращает список сохраненных видео выбранного канала. Поддерживает пагинацию. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "data": [ -> { -> "vod_id": "vod-8877", -> "title": "Grand Final CS2", -> "duration_seconds": 14400, // Длительность: 4 часа -> "views": 5400, -> "thumbnail_url": "https://cdn.stremo.com/vod-thumbs/vod-8877.jpg", -> "created_at": "2026-05-19T18:00:00Z" -> } -> ], -> "next_cursor": "QWVyZ..." -> } -> ``` - ->[!CAUTION] -> **404 Not Found** -> ```json -> { -> "error": { -> "code": "CHANNEL_NOT_FOUND", -> "message": "Запрашиваемый канал не существует", -> "request_id": "req-11a2b-3c88" -> } -> } -> ``` - ---- - -## **Ingest-service** - -> Микросервис отвечает за прием видео-потока по протоколам RTMP/SRT. Включает API для управления настройками потока. - -#### *Получение ключа трансляции* `GET /ingest/me/key` - -Возвращает секретный Stream Key для настройки OBS. - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "stream_url": "rtmp://ingest.stremo.com/live", -> "stream_key": "live_123456_abcdef9876543210" // Секретный ключ! -> } -> ``` - ->[!CAUTION] -> **401 Unauthorized** -> ```json -> { -> "error": { -> "code": "UNAUTHORIZED", -> "message": "Для получения ключа трансляции необходимо авторизоваться", -> "request_id": "req-22b3c-4d99" -> } -> } -> ``` - ---- - -## **Transcoder-service (Внутреннее API)** - -> Микросервис принимает команды от Ingest Service и отвечает за запуск изолированных воркеров FFmpeg. Недоступен из внешней сети. - -#### *Вебхук готовности HLS-плейлиста* `POST /internal/v1/transcoder/status` - -Отправляется воркером, когда видеопоток успешно сконвертирован в различные качества (1080p, 720p) и HLS (.m3u8) чанки загружены в S3. - -**Request Body** - -| Поле | Тип | Обяз. | Описание и Валидация | -| :--- | :--- | :--- | :--- | -| `stream_id` | `string` | Да | ID текущей трансляции. | -| `status` | `string` | Да | Статус транскодинга: `processing`, `completed`, `error`. | -| `master_playlist_url` | `string` | Нет | URL к HLS master.m3u8 плейлисту. | - -Примеры ответов - ->[!TIP] -> **200 OK** -> ```json -> { -> "message": "Status updated successfully", -> "ack": true -> } -> ``` - ->[!CAUTION] -> **422 Unprocessable Entity** -> ```json -> { -> "error": { -> "code": "INVALID_PAYLOAD", -> "message": "Отсутствуют обязательные параметры", -> "request_id": "req-internal-abc-123" -> } -> } -> ``` - ---- - -## **Smtp-service (Внутреннее API)** - -> Микросервис изолирует всю логику по отправке транзакционных писем. Вызывается по сети кластера (cluster.local) другими сервисами (например, Auth Service). - -#### *Отправка письма по шаблону* `POST /internal/v1/mail/send` - -Добавляет письмо в очередь на отправку. - -**Request Body** - -| Поле | Тип | Обяз. | Описание | -| :--- | :--- | :--- | :--- | -| `to` | `string` | Да | Email получателя. | -| `template_id` | `string` | Да | ID HTML-шаблона. | -| `variables` | `object` | Да | JSON-объект с переменными для подстановки в шаблон. | - -Примеры ответов - ->[!TIP] -> **202 Accepted** -> ```json -> { -> "status": "queued", -> "job_id": "job-555123", // Внутренний ID задачи в брокере (Kafka/Redis) -> "message": "Письмо добавлено в очередь на отправку" -> } -> ``` - ->[!CAUTION] -> **400 Bad Request** -> ```json -> { -> "error": { -> "code": "TEMPLATE_ERROR", -> "message": "Не переданы обязательные переменные шаблона: username", -> "request_id": "req-internal-xyz-999" -> } -> } -> ``` -> -> **500 Internal Server Error** -> ```json -> { -> "error": { -> "code": "SMTP_GATEWAY_ERROR", -> "message": "Не удалось подключиться к внешнему провайдеру (SendGrid/Mailgun)", -> "request_id": "req-internal-err-500" -> } -> } -> ``` - - ---- -*by finnik* diff --git a/docs/Archicture.md b/docs/Archicture.md deleted file mode 100644 index c14fc39..0000000 --- a/docs/Archicture.md +++ /dev/null @@ -1,376 +0,0 @@ -# Архитектура Платформы STREMO - -![Architecture Header](../img/arch-md.png) - ->[!IMPORTANT] -> Данная спецификация является единым источником истины (SSOT) для высокоуровневой и низкоуровневой архитектуры стриминговой платформы STREMO. Документ описывает глобальную топологию, паттерны проектирования, потоки данных и внутреннее устройство каждого ключевого микросервиса. - ->[!NOTE] -> Основной технологический стек бэкенда: **C++20**. Выбор обусловлен необходимостью детерминированного управления памятью, отсутствием пауз сборщика мусора (GC) при обработке видеопотоков в реальном времени и максимальной производительностью сетевого слоя (Boost.Asio / gRPC). - ---- - -## **1. Глобальная Топология Платформы** - -Система разделена на независимые домены (Video, Core, Interactive). Весь внешний трафик проходит через Edge-балансировщики, фильтруется и перенаправляется на внутренние C++ микросервисы. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 160, "nodeSpacing": 15}}}%% -flowchart LR - classDef default fill:#fff,stroke:#000,stroke-width:2px,color:#000 - classDef dashedRed fill:#fff,stroke:#d63031,stroke-width:2px,stroke-dasharray: 6 6,color:#d63031,rx:15,ry:15 - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Streamers("Streamers\n(OBS)"):::default - Users("Viewers\n(Web/App)"):::default - - Streamers -->|"RTMP: TCP 1935"| TCPProxy("tcp_proxy\n(HAProxy)"):::dashedRed - TCPProxy -->|"Raw Stream"| Ingest("ingest_service\n(C++)"):::solidBlack - Ingest -->|"IPC / Shared Mem"| Transcoder("transcoder_service\n(FFmpeg)"):::solidBlack - Transcoder -->|"PUT /hls/*.ts"| S3("s3_storage\n(Multi-AZ)"):::storage - - Users -->|"GET /master.m3u8"| CDN("cloud_cdn\n(Edge Cache)"):::dashedBlack - CDN -.->|"Cache Miss"| S3 - - Users -->|"REST API (HTTPS)"| Nginx("nginx\n(Ingress, WAF)"):::dashedRed - Nginx -->|"Rate Limited"| BFF("BFF_service\n(API Gateway)"):::dashedBlack - - BFF -->|"gRPC: VerifyToken"| Auth("auth_service"):::solidBlack - BFF -->|"gRPC: GetProfile"| User("user_service"):::solidBlack - BFF -->|"gRPC: GetMeta"| StreamMeta("stream_meta_service"):::solidBlack - BFF -->|"gRPC: GetClips"| VOD("vod_service"):::solidBlack - BFF -->|"gRPC: Checkout"| Billing("billing_service"):::solidBlack - - Auth & User & StreamMeta & VOD & Billing --> DB("postgresql_cluster\n(Sharded by: channel_id)"):::storage - - Users -.->|"WSS: /ws/events"| Nginx - Nginx -->|"Upgrade WSS"| Chat("chat_service\n(WebSockets)"):::solidBlack - - Chat -->|"PUBLISH chat:{id}"| Redis("redis_cluster\n(Pub/Sub & Hot State)"):::storage - Chat -.->|"gRPC: Check Spam"| Mod("mod_service\n(ML Filters)"):::solidBlack - - Ingest -.->|"gRPC: Report CCV"| Analytics("analytics_service\n(Aggregator)"):::solidBlack - Analytics -->|"INSERT Batch (5s)"| Clickhouse("clickhouse\n(ReplicatedMergeTree)"):::storage - - Ingest -.->|"Verify Stream Key"| StreamMeta - Ingest -.->|"stream.started"| Kafka("kafka_bus\n(Partitions: 32)"):::storage - Billing -.->|"payment.received"| Kafka - Mod -.->|"user.banned"| Kafka - - Kafka -.->|"Consume Events"| Notify("notify_service\n(WebPush/SSE)"):::solidBlack - Notify -.->|"Push Alert"| Users -``` - ---- - -## **2. Детальная Архитектура Доменов и Микросервисов** - -Каждый микросервис спроектирован с учетом изоляции отказов (Fault Isolation) и возможности независимого горизонтального масштабирования (Horizontal Pod Autoscaling). - -### **2.1. Домен Стриминга: Видео Пайплайн (Ingest & Transcoder)** - -Самый ресурсоемкий узел платформы. Цель пайплайна — получить сырой видеопоток, конвертировать его в различные разрешения (Adaptive Bitrate) и доставить зрителям с минимальной задержкой. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 30}}}%% -flowchart TD - classDef dashedRed fill:#fff,stroke:#d63031,stroke-width:2px,stroke-dasharray: 6 6,color:#d63031,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Input("TCP Payload (RTMP/SRT)"):::dashedRed --> Ingest("Ingest Service\n(Asio Event Loop)"):::solidBlack - - subgraph K8s Node: Compute Optimized - Ingest -->|"Spawn Process"| Supervisor("Transcoder Supervisor\n(Subprocess Manager)"):::solidBlack - Supervisor -->|"Pipe stdout/stdin"| FFmpeg1("FFmpeg Worker 1\n(1080p)"):::dashedRed - Supervisor -->|"Pipe stdout/stdin"| FFmpeg2("FFmpeg Worker 2\n(720p)"):::dashedRed - - FFmpeg1 -->|"Write Segments"| Tmpfs("RAM Disk (/tmpfs)"):::storage - FFmpeg2 -->|"Write Segments"| Tmpfs - end - - Tmpfs -->|"S3 Multipart Upload"| S3("Object Storage"):::storage - Ingest -.->|"gRPC Call"| StreamMeta("Stream Meta Service"):::solidBlack -``` - -**Как это работает:** -1. **Ingest Service (Прием):** Терминирует RTMP соединение. Первым делом делает синхронный gRPC вызов в `stream-meta-service` для валидации Stream Key. Если ключ неверен, TCP соединение моментально сбрасывается. -2. **Transcoder Supervisor:** Если ключ валиден, Ingest порождает (fork/exec) новые процессы FFmpeg. Связь между Ingest и FFmpeg идет через анонимные пайпы (IPC/Pipes), что исключает сетевые задержки внутри узла. -3. **HLS & RAM Disk:** FFmpeg нарезает видео на чанки (2 секунды). Чтобы не изнашивать SSD сервера тысячами операций записи в секунду, чанки пишутся в оперативную память (RAM Disk / `tmpfs`). -4. **Upload:** Фоновый воркер асинхронно выгружает готовые файлы из `tmpfs` в S3. По завершении загрузки манифеста вызывается webhook-уведомление внутреннего API платформы. - ->[!CAUTION] -> **Привязка к железу (Node Affinity)** -> Поды Ingest и Transcoder разворачиваются строго на узлах Kubernetes с маркировкой `video-optimized`. Эти узлы имеют мощные процессоры и аппаратные энкодеры GPU (NVENC), проброшенные в контейнер через драйверы NVIDIA. - ---- - -### **2.2. Домен Интерактива: Чат и Модерация (Realtime Pub/Sub)** - -Домен чата должен выдерживать скачкообразные нагрузки (Thundering Herd) во время масштабных киберспортивных турниров (до 500,000 онлайна). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 120, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Viewer1("Viewer 1"):::dashedBlack -->|"WSS /ws/chat"| ChatPod1("Chat Service Pod 1"):::solidBlack - Viewer2("Viewer 2"):::dashedBlack -->|"WSS /ws/chat"| ChatPod2("Chat Service Pod 2"):::solidBlack - - ChatPod1 -->|"Validate"| ModSvc("Moderation Svc\n(NLP Spam Filter)"):::solidBlack - - ChatPod1 -->|"PUBLISH chat:channel_id"| Redis("Redis Cluster\n(Pub/Sub)"):::storage - - Redis -->|"SUBSCRIBE chat:channel_id"| ChatPod2 - Redis -->|"SUBSCRIBE chat:channel_id"| ChatPod1 - - ChatPod2 -->|"Broadcast WS"| Viewer2 - - ChatPod1 -.->|"Batch INSERT (500 ms)"| Citus("Postgres\n(Citus Shard)"):::storage -``` - -**Как это работает:** -1. **WebSocket Балансировка:** Балансировщик нагрузки (Ingress) распределяет WSS-соединения зрителей случайным образом по сотням экземпляров (подов) `chat-service`. -2. **Синхронизация через Redis:** Когда зритель отправляет сообщение на `ChatPod1`, этот под обязан доставить его всем остальным зрителей в этой же комнате. Под публикует сообщение в Redis-канал `chat:`. -3. **Рассылка (Fan-Out):** Все остальные поды (например, `ChatPod2`), которые держат соединения зрителей из этого же канала, подписаны на этот Redis-канал. Они получают событие и пересылают его в открытые WebSocket-сокеты своих клиентов. -4. **Сжатие базы данных:** Чтобы не перегрузить базу одиночными инсертами, каждый под накапливает сообщения в кольцевом буфере памяти и сбрасывает их в PostgreSQL одним `COPY` или многострочным `INSERT` запросом раз в полсекунды. - ->[!TIP] -> **Автоматический Slow Mode** -> Если Redis фиксирует превышение лимита (например, больше 100 сообщений в секунду для одного канала), `chat-service` автоматически включает "Slow Mode", заставляя клиентов выжидать паузу перед отправкой, чтобы предотвратить деградацию UI на фронтенде. - ---- - -### **2.3. Домен Биллинга: Гарантия согласованности (Transactional Outbox)** - -Биллинг работает с реальными деньгами и внутренней валютой (Bits). Ключевая проблема здесь — двойная запись (Dual Write): необходимость изменить баланс в базе данных и гарантированно отправить уведомление в шину событий (Kafka). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart TD - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - API("REST API (Donate)"):::dashedBlack --> Billing("Billing Service\n(Transaction Manager)"):::solidBlack - - subgraph Transaction Boundary - Billing -->|"1. BEGIN"| DB("PostgreSQL"):::storage - Billing -->|"2. UPDATE users SET bits = bits - 100"| DB - Billing -->|"3. INSERT INTO outbox (event_data)"| DB - Billing -->|"4. COMMIT"| DB - end - - DB -.->|"Logical Replication (WAL)"| Debezium("Debezium Relay\n(CDC)"):::solidBlack - Debezium -->|"5. Produce"| Kafka("Kafka Broker\n(Topic: stream.alerts)"):::storage - - Kafka -->|"6. Consume"| Notify("Notify Service"):::solidBlack - Notify -->|"7. Send Alert"| Streamer("Streamer Screen"):::dashedBlack -``` - -**Как это работает:** -1. **ACID Транзакция:** Биллинг открывает транзакцию в PostgreSQL. Он списывает деньги со счета и *в этой же транзакции* записывает факт совершения платежа (событие для алерта) в системную таблицу `outbox`. -2. **Гарантия целостности:** Если транзакция откатится (сбой сети, нехватка средств), событие не сохранится в `outbox`. Если зафиксируется — событие гарантированно в базе. -3. **CDC (Change Data Capture):** Процесс Debezium непрерывно читает журнал упреждающей записи (WAL) PostgreSQL. Как только в таблице `outbox` появляется новая строка, Debezium конвертирует ее в событие Kafka. -4. **Семантика At-Least-Once:** Даже если `billing-service` упадет сразу после `COMMIT`, система CDC все равно отправит событие в Kafka, гарантируя, что алерт доната обязательно появится на экране стримера. - ---- - -## **3. Архитектура Остальных Микросервисов** - -Помимо трех основных высоконагруженных доменов, описанных выше, платформа состоит из набора специализированных микросервисов. Ниже представлены схемы их внутреннего устройства. - -### **3.1. Auth Service (Сервис Аутентификации)** - -Отвечает за генерацию токенов, хеширование паролей и интеграцию с провайдерами связи для 2FA. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - BFF("API Gateway"):::dashedBlack -->|"REST: /login"| Auth("Auth Service\n(Argon2 Hashing)"):::solidBlack - - Auth -->|"Validate Credentials"| DB("PostgreSQL\n(Users Table)"):::storage - Auth -->|"Generate JWT"| Auth - - Auth -->|"Set Rate Limit"| Redis("Redis\n(Token Bucket)"):::storage - - BFF -->|"REST: /mobile-id"| Auth - Auth -->|"gRPC: Send SMS/Push"| SmtpSvc("SMTP / Telecom Provider"):::solidBlack - Auth -->|"Store 2FA Session"| Redis -``` - -### **3.2. User Profile Service (Сервис Профилей)** - -Обрабатывает запросы на получение публичной информации о каналах и управление подписками (фолловерами). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - BFF("API Gateway"):::dashedBlack -->|"GET /users/me"| UserSvc("User Profile Svc"):::solidBlack - - UserSvc -->|"Check Cache"| Redis("Redis Cluster"):::storage - Redis -.->|"Cache Miss"| DB("PostgreSQL\n(Global)"):::storage - DB -.->|"Update Cache"| Redis - - BFF -->|"POST /avatar"| UserSvc - UserSvc -->|"Resize & Compress"| UserSvc - UserSvc -->|"Multipart Upload"| S3("S3 Storage"):::storage -``` - -### **3.3. Stream Meta Service (Сервис Метаданных)** - -Хранит каталог стримов, названия, категории и обеспечивает быстрый поиск (Keyset Pagination) для главной страницы. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - BFF("API Gateway"):::dashedBlack -->|"GET /streams/live"| MetaSvc("Stream Meta Svc"):::solidBlack - IngestSvc("Ingest Service"):::dashedBlack -->|"gRPC: Verify Key"| MetaSvc - - MetaSvc -->|"Query Directory"| DB("PostgreSQL\n(Sharded)"):::storage - MetaSvc -->|"Get Live CCV"| Redis("Redis\n(Live Viewers Cache)"):::storage - - BFF -->|"PUT /meta"| MetaSvc - MetaSvc -->|"Update DB"| DB - MetaSvc -->|"Event: MetaUpdated"| Kafka("Kafka Bus"):::storage -``` - -### **3.4. Moderation Service (Сервис Модерации)** - -Работает в связке с чатом. Использует NLP (Natural Language Processing) модели для автоматической фильтрации спама. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart TD - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - ChatSvc("Chat Service"):::dashedBlack -->|"gRPC: CheckSpam(text)"| ModSvc("Moderation Svc\n(C++ Orchestrator)"):::solidBlack - BFF("API Gateway"):::dashedBlack -->|"POST /ban"| ModSvc - - ModSvc -->|"TensorRT Inference"| NLP("ML Model\n(Spam Classifier)"):::solidBlack - - ModSvc -->|"Publish 'Ban' Event"| Redis("Redis Pub/Sub\n(Disconnect WS)"):::storage - ModSvc -->|"Log Action"| DB("PostgreSQL\n(Audit Log)"):::storage -``` - -### **3.5. Analytics Service (Сервис Аналитики)** - -Считает онлайн (CCV) и строит агрегированные графики для стримеров. Из-за высоких нагрузок на запись использует ClickHouse. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - IngestSvc("Ingest Service"):::dashedBlack -->|"gRPC: Report Ping (15s)"| Analytics("Analytics Svc\n(Aggregator)"):::solidBlack - - Analytics -->|"Update Live CCV"| Redis("Redis"):::storage - Analytics -->|"Buffer Metrics in Memory"| Analytics - - Analytics -->|"Bulk INSERT (Every 5s)"| ClickHouse("ClickHouse\n(Columnar DB)"):::storage - - BFF("API Gateway"):::dashedBlack -->|"GET /summary"| Analytics - Analytics -->|"Fast SELECT"| ClickHouse -``` - -### **3.6. Notification Service (Сервис Уведомлений)** - -Читает события из шины данных и превращает их в In-App уведомления или мобильные Push-сообщения. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Kafka("Kafka Bus\n(stream.started)"):::storage -->|"Consume"| Notify("Notification Svc"):::solidBlack - - Notify -->|"Find Followers"| DB("PostgreSQL"):::storage - Notify -->|"Save In-App Alert"| DB - - Notify -->|"Send Payload"| FCM("Firebase / APNs\n(Mobile Push)"):::solidBlack - Notify -->|"SSE / WebPush"| Users("Web Clients"):::dashedBlack -``` - -### **3.7. VOD Manager Service (Управление Записями)** - -Предоставляет API для доступа к прошлым стримам (VOD) и создания коротких клипов из длинных трансляций. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - BFF("API Gateway"):::dashedBlack -->|"GET /vods"| VOD("VOD Manager Svc"):::solidBlack - Transcoder("Transcoder Svc"):::dashedBlack -->|"Webhook: VOD Ready"| VOD - - VOD -->|"Save Metadata"| DB("PostgreSQL"):::storage - - BFF -->|"POST /clips"| VOD - VOD -->|"Schedule Clip Job"| Kafka("Kafka Bus"):::storage -``` - -### **3.8. SMTP Service (Внутренний Почтовый Шлюз)** - -Изолирует логику рендеринга HTML шаблонов и общения с внешними провайдерами электронной почты. Никакой другой сервис не имеет прямого доступа к интернету для отправки писем. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - CoreSvc("Any Core Service\n(Auth, Billing)"):::dashedBlack -->|"POST /internal/mail"| SMTP("SMTP Service\n(Template Engine)"):::solidBlack - - SMTP -->|"Enqueue Job"| Redis("Redis Queue"):::storage - - SMTP -->|"Process Queue"| Mailgun("External Provider\n(SendGrid / Mailgun)"):::dashedBlack -``` - ---- - -## **4. Хранение Данных и Шардирование** - -Платформа избегает единой точки отказа (SPOF) на уровне хранения данных. - -* **Global Database (PostgreSQL):** Содержит критически важные, но редко изменяемые данные. Это таблицы пользователей, паролей, ролей, балансов и истории транзакций. База работает в режиме Primary-Replica. -* **Sharded Database (PostgreSQL + Citus Data):** Содержит огромные объемы данных: историю чата, VOD-клипы, статистику и просмотры. Данные горизонтально распределены по нескольким узлам кластера на основе ключа распределения (`channel_id` или `stream_id`). Запросы маршрутизируются на нужный шард прозрачно для приложения. -* **ClickHouse (Аналитика):** База данных столбцового типа (Columnar). Используется сервисом `analytics-service` для агрегации метрик. Принимает широкие инсерты (CCV, показы рекламы) и позволяет строить графики для дашборда стримера за миллисекунды. - ---- - -## **5. Сводный Технологический Стек** - -| Слой | Технология | Бизнес-обоснование выбора | -| :--- | :--- | :--- | -| **Backend Core** | C++20 | Ручное управление ресурсами. Использование корутин (coroutines) и современных библиотек (Boost.Asio) для предсказуемой задержки и высокой пропускной способности. | -| **Внутреннее RPC** | gRPC (Protobuf) | Строгая типизация контрактов между командами разработчиков. Бинарная сериализация снижает нагрузку на сеть в 10 раз по сравнению с REST/JSON. | -| **API Gateway** | Go / Node.js | Паттерн BFF (Backend for Frontend). Терминирует внешние JWT токены, агрегирует данные с разных gRPC сервисов и отдает фронтенду удобный JSON. | -| **Видео Ядро** | FFmpeg (C API) | Индустриальный стандарт транскодинга. Позволяет тонко управлять пресетами кодеков (x264/NVENC) для достижения оптимального качества ABR. | -| **Кэш / PubSub** | Redis 7 (Cluster) | Хранение "горячих" данных (сессии, профили), реализация Token Bucket (Rate Limit) и шина обмена сообщениями для чата (Pub/Sub). | -| **Брокер Событий** | Apache Kafka | Развязка микросервисов (Decoupling) и гарантия доставки (Event Sourcing). Выдерживает колоссальные объемы записей без деградации производительности. | -| **Инфраструктура** | Kubernetes (K8s) | Оркестрация контейнеров, изоляция ресурсов, автоматический Horizontal Pod Autoscaling (HPA) воркеров в зависимости от нагрузки. | diff --git a/docs/CI-CD.md b/docs/CI-CD.md deleted file mode 100644 index eabcc2b..0000000 --- a/docs/CI-CD.md +++ /dev/null @@ -1,108 +0,0 @@ -# Непрерывная Интеграция и Качество Кода (CI Pipeline) - ->[!IMPORTANT] -> Данный документ детализирует процессы, происходящие с кодом до его попадания в среду развертывания (которая описана в `Deploy.md`). Строгий CI-пайплайн гарантирует, что в Production попадет только проверенный, безопасный и оптимизированный код. - ---- - -## **1. Архитектура CI Пайплайна (GitLab CI / GitHub Actions)** - -Пайплайн запускается автоматически при создании Pull Request (Merge Request) или коммите в ветку `main`. Для C++ микросервисов процесс сборки и тестирования разделен на жесткие этапы (Stages), каждый из которых блокирует дальнейшее продвижение при возникновении ошибки. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 20}}}%% -flowchart TD - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Trigger("Commit to 'main'\nor PR Created"):::dashedBlack - - subgraph Stage 1: Build & Lint - direction LR - ClangTidy("Clang-Tidy\n(Static Analysis)"):::solidBlack - CMake("CMake Build\n(GCC / Clang)"):::solidBlack - end - - subgraph Stage 2: Testing - direction LR - GTest("Unit Tests\n(GoogleTest)"):::solidBlack - Valgrind("Valgrind\n(Memory Leaks Check)"):::storage - end - - subgraph Stage 3: Security & SAST - direction LR - Sonar("SonarQube\n(Code Quality)"):::solidBlack - Trivy("Trivy\n(Vulnerability Scan)"):::storage - end - - subgraph Stage 4: Package & Release - direction LR - Docker("Docker Build\n(Multi-stage)"):::solidBlack - Push("Push to Registry\n(ECR / Harbor)"):::storage - end - - Trigger --> Stage1 - Stage1 -->|"If Passed"| Stage2 - Stage2 -->|"If Passed"| Stage3 - Stage3 -->|"If Passed"| Stage4 -``` - ---- - -## **2. Детализация Этапов (Stages)** - -### **2.1. Сборка и Линтинг (Build & Lint)** - -Кодовая база C++20 требует строгих стандартов форматирования и проверки на этапе компиляции. - -* **Clang-Format:** Проверяет код на соответствие `.clang-format` файлу проекта (стандарт Google C++ Style Guide с локальными модификациями). -* **Clang-Tidy:** Ловит потенциальные баги до компиляции (например, неинициализированные переменные, опасные приведения типов, нарушения `const-correctness`). -* **Компиляция:** Сборка происходит через CMake. В пайплайне всегда включен флаг `-Werror` (считать Warnings ошибками), чтобы код с предупреждениями не мог быть слит в `main`. - -### **2.2. Тестирование и Анализ Памяти (Testing)** - -Отказоустойчивость сервисов реального времени (In-Memory буферы, WebSockets) критически зависит от работы с памятью. - -* **GoogleTest (GTest):** Запускает набор Unit-тестов для бизнес-логики. -* **Valgrind / AddressSanitizer (ASan):** Бинарный файл запускается в специальном окружении для поиска утечек памяти (Memory Leaks), обращений к освобожденной памяти (Use-After-Free) и состояний гонки (Race Conditions). - ->[!CAUTION] -> **Блокировка пайплайна (Quality Gate)** -> Покрытие Unit-тестами (Code Coverage) должно быть не ниже 80%. Если покрытие падает ниже этого порога, пайплайн завершается с ошибкой (`exit 1`), и слияние ветки (Merge Request) автоматически блокируется. - ---- - -## **3. Безопасность и SAST (Security Scanning)** - -STREMO обрабатывает платежные данные и трансляции, поэтому безопасность интегрирована прямо в процесс сборки (Shift-Left Security). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - SourceCode("Source Code"):::dashedBlack --> SAST("SonarQube\n(Code Vulnerabilities)"):::solidBlack - DockerImage("Compiled Docker Image"):::dashedBlack --> Trivy("Trivy Scanner\n(OS Vulnerabilities)"):::storage - - SAST -->|"Quality Gate"| Result("Merge Request\nStatus"):::solidBlack - Trivy -->|"Block High/Critical"| Result -``` - -* **SonarQube (SAST):** Анализирует исходный код на уязвимости (например, SQL Injection, Buffer Overflow) и технический долг (Code Smell). -* **Trivy (Container Scanning):** После того как Docker-образ собран, Trivy сканирует его слои на предмет уязвимостей в системных библиотеках (CVE). Если найдены уязвимости уровня `CRITICAL` или `HIGH` в базовом образе, пуш в Registry блокируется. - ---- - -## **4. Оптимизация Docker-образов (Multi-Stage Build)** - -Поскольку C++ требует тяжелых компиляторов и библиотек (Boost, FFmpeg dev-headers), мы используем **Multi-Stage Builds** для создания финального образа, который пойдет в Production. - -1. **Stage 1 (Builder):** Использует тяжелый образ (`ubuntu` или `debian` с GCC/Clang, CMake, Conan/vcpkg). Здесь компилируется бинарный файл. -2. **Stage 2 (Runtime):** Использует минималистичный образ `distroless/cc` (образ без shell, пакетного менеджера и лишних библиотек). В него копируется только скомпилированный бинарный файл и нужные `.so` файлы. - ->[!TIP] -> **Преимущества Distroless:** -> Финальный образ микросервиса весит около 20-30 МБ вместо 1 ГБ+. Это кардинально ускоряет время запуска пода (Cold Start) в Kubernetes во время автомасштабирования (HPA) и сводит площадь атаки (Attack Surface) практически к нулю (внутри контейнера нет даже `bash`). \ No newline at end of file diff --git a/docs/DataBase.md b/docs/DataBase.md deleted file mode 100644 index de98464..0000000 --- a/docs/DataBase.md +++ /dev/null @@ -1,169 +0,0 @@ -# Архитектура Баз Данных STREMO - -![](../img/db-md.png) - ->[!IMPORTANT] -> База данных — самый критичный компонент платформы. STREMO использует подход гибридного хранения данных (Polyglot Persistence). Документ описывает разделение на Глобальный кластер (для транзакций) и Шардированный кластер (для тяжелых данных), а также механизмы репликации и отказоустойчивости. - ---- - -## **1. Глобальная Топология Данных** - -Чтобы избежать бутылочного горлышка (Bottleneck) при росте платформы, данные строго разделены на три независимых кластера в зависимости от паттерна их использования (Read-Heavy, Write-Heavy или Analytical). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 20}}}%% -flowchart TD - classDef cluster fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#334155,rx:10,ry:10,stroke-dasharray: 5 5 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - BFF("API Gateway / Microservices"):::solidBlack - - subgraph Cluster_Global ["1. Global Transactional DB (PostgreSQL)"] - direction TB - DB_Users("Users, Auth, Billing"):::storage - end - - subgraph Cluster_Sharded ["2. Sharded Distributed DB (Citus / Postgres)"] - direction TB - DB_Streams("Chat History, VODs, Stream Meta"):::storage - end - - subgraph Cluster_Analytics ["3. OLAP Analytics (ClickHouse)"] - direction TB - DB_Metrics("CCV, Ad Impressions, Traffic"):::storage - end - - BFF -->|"ACID Transactions"| Cluster_Global - BFF -->|"High Write Throughput"| Cluster_Sharded - BFF -->|"Analytical Queries"| Cluster_Analytics -``` - ---- - -## **2. Глобальный Кластер (PostgreSQL)** - -Этот кластер хранит "золотые данные" платформы. Здесь происходят финансовые операции, хранятся профили, пароли и настройки. -* **Паттерн:** Read-Heavy (часто читают профили, редко меняют). -* **Требования:** Строгий ACID, гарантия отсутствия потерь при сбоях. - -### **2.1 Схема Репликации (High Availability)** - -Для обеспечения доступности (HA) используется асинхронная потоковая репликация (Streaming Replication) с автоматическим переключением (Failover) через Patroni или PgBouncer. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 40}}}%% -flowchart TD - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Router("PgBouncer / HAProxy\n(Connection Pooling)"):::solidBlack - - Master("PostgreSQL PRIMARY\n(Read / Write)"):::storage - Replica1("PostgreSQL REPLICA 1\n(Read-Only)"):::storage - Replica2("PostgreSQL REPLICA 2\n(Read-Only)"):::storage - - Router -->|"Writes (UPDATE/INSERT)"| Master - Router -.->|"Reads (GET /users)"| Replica1 - Router -.->|"Reads (GET /users)"| Replica2 - - Master ==>|"WAL Streaming (Async)"| Replica1 - Master ==>|"WAL Streaming (Async)"| Replica2 -``` - -### **2.2 Основные Таблицы Глобального Кластера** - -* `users`: ID, email, хэш пароля, дата регистрации. -* `profiles`: bio, avatar_url, настройки уведомлений. -* `wallets`: баланс fiat, баланс Bits (строгие транзакционные блокировки `SELECT FOR UPDATE`). -* `outbox_events`: системная таблица для паттерна Transactional Outbox (события для отправки в Kafka). -* `followers`: связь Many-to-Many между зрителями и каналами. - ---- - -## **3. Шардированный Кластер (Citus Data)** - -Самая большая проблема стриминговой платформы — чат. Если 100,000 человек одновременно пишут в разные чаты, один мастер-сервер PostgreSQL не выдержит нагрузки на запись (Write-Heavy). Мы решаем это с помощью расширения **Citus**, которое превращает PostgreSQL в распределенную СУБД. - -### **3.1 Принцип Шардирования (Horizontal Scaling)** - -Данные разбиваются на части (шарды) и распределяются по разным физическим серверам (Worker Nodes). -* **Ключ распределения (Distribution Key):** `channel_id`. -* **Почему `channel_id`?** Это гарантирует, что вся история чата конкретного стримера, все его клипы и метаданные его стрима будут лежать на **одном физическом диске**. Это позволяет делать быстрые локальные `JOIN` запросы без пересылки данных по сети между нодами. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 15}}}%% -flowchart TD - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - App("Chat Service / Stream Meta Svc"):::solidBlack - - Coordinator("Citus Coordinator Node\n(Query Router)"):::storage - - App -->|"INSERT INTO chat_messages\n(channel_id=1)"| Coordinator - App -->|"INSERT INTO chat_messages\n(channel_id=9)"| Coordinator - App -->|"INSERT INTO chat_messages\n(channel_id=4)"| Coordinator - - subgraph Citus Worker Nodes - direction LR - Worker1("Worker Node A\n(Shards for ch: 1, 2, 3)"):::storage - Worker2("Worker Node B\n(Shards for ch: 4, 5, 6)"):::storage - Worker3("Worker Node C\n(Shards for ch: 7, 8, 9)"):::storage - end - - Coordinator ==>|"Routes ch=1"| Worker1 - Coordinator ==>|"Routes ch=4"| Worker2 - Coordinator ==>|"Routes ch=9"| Worker3 -``` - -### **3.2 Основные Таблицы Шардированного Кластера** - -* `chat_messages`: ID сообщения, `channel_id` (Sharding Key), `user_id`, текст, timestamp. -* `streams`: ID трансляции, `channel_id` (Sharding Key), title, category, status. -* `vods`: сохраненные видео, привязаны к `channel_id`. -* `moderation_actions`: баны и таймауты (распределены по `channel_id` для быстрого доступа во время стрима). - ->[!TIP] -> **Автоматическая балансировка** -> При добавлении новых серверов в кластер, Citus автоматически в фоновом режиме (не прерывая работу платформы) перенесет часть шардов на новые сервера, размазывая нагрузку. - ---- - -## **4. ClickHouse (Слой Аналитики)** - -Аналитика требует совершенно другого подхода. Стример хочет видеть график "Сколько зрителей было на каждой минуте моего 10-часового стрима". PostgreSQL будет выполнять такой запрос очень долго, сканируя миллионы строк. -**ClickHouse** решает это благодаря столбцовому хранению и векторизованным вычислениям. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 20}}}%% -flowchart LR - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Ingest("Ingest Svc"):::solidBlack -->|"Publishes"| Kafka("Kafka Bus\n(metrics_topic)"):::storage - - Kafka -->|"Consume Batch\n(100k events/sec)"| CH_Buffer("ClickHouse\nKafka Engine Table"):::storage - - CH_Buffer -->|"Materialized View\n(Aggregates by Minute)"| CH_Final("ClickHouse\nMergeTree (Fast Reads)"):::storage - - BFF("API Gateway"):::solidBlack -->|"GET /dashboard"| CH_Final -``` - -### **4.1 Почему ClickHouse?** -1. **Огромная скорость вставки:** Способен "заглатывать" сотни тысяч метрик в секунду напрямую из Kafka (через встроенный Kafka Table Engine). -2. **Агрегация на лету:** Данные о зрителях сбрасываются каждую секунду. ClickHouse (через Materialized Views) автоматически сжимает их, вычисляя средний и пиковый онлайн (CCV) за каждую минуту. -3. **Экономия диска:** Столбцовые данные отлично сжимаются (в 3-5 раз лучше, чем в PostgreSQL). - ---- - -## **5. Механизмы Кэширования (Redis Cluster)** - -Базы данных не должны обрабатывать 100% трафика. Все частые запросы экранируются кластером Redis 7. - -* **Token Bucket:** Хранит лимиты Rate Limit (защита от DDoS). -* **Pub/Sub:** Мгновенная рассылка сообщений чата между подами Kubernetes (данные не сохраняются в Redis, только маршрутизируются). -* **Сессии и JWT:** Список отозванных токенов (Blacklist). -* **Live Directory:** Список активных стримов для Главной страницы обновляется в Redis каждую секунду. БД не участвует в рендеринге каталога. diff --git a/docs/Deploy.md b/docs/Deploy.md deleted file mode 100644 index 7ae89b0..0000000 --- a/docs/Deploy.md +++ /dev/null @@ -1,106 +0,0 @@ -# Инфраструктура и Процесс Развертывания (GitOps CI/CD) - -![](../img/deploy-md.png) - ->[!IMPORTANT] -> Инфраструктура STREMO управляется по принципу **GitOps**. Это означает, что конфигурация всего Kubernetes-кластера (Deployment, Service, ConfigMap, Ingress) описана в виде манифестов в репозитории `infra`. Прямые изменения кластера через `kubectl apply` строго запрещены. - ---- - -## **1. CI/CD Пайплайн (Continuous Integration / Deployment)** - -Мы используем двухконтурный процесс: GitLab CI (или GitHub Actions) для сборки кода (CI) и ArgoCD / FluxCD для синхронизации состояния кластера (CD). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Dev("Developer"):::solidBlack -->|"1. Push Code"| AppRepo("App Repo\n(C++ / Go)"):::storage - - AppRepo -->|"2. Trigger CI"| CI("GitLab CI\n(Build, Test, Lint)"):::solidBlack - CI -->|"3. Push Image"| Registry("Docker Registry\n(ECR / Harbor)"):::storage - - CI -->|"4. Update Image Tag"| InfraRepo("Infra Repo\n(Kustomize / Helm)"):::storage - - ArgoCD("ArgoCD\n(In Kubernetes)"):::dashedBlack -.->|"5. Detect Drift (Pull)"| InfraRepo - - ArgoCD ==>|"6. Sync Manifests"| K8s("Production K8s Cluster"):::storage -``` - -### **1.1. Этап CI (Сборка)** -1. При пуше в ветку `main` микросервиса запускается пайплайн. -2. Запускаются Unit-тесты и статический анализатор кода (SonarQube). -3. Собирается Docker-образ. В качестве базового образа для C++ микросервисов используется `alpine` или `distroless` для минимизации размера и поверхности атак. -4. Собранный образ пушится в Container Registry с тегом коммита (например, `v1.2.4-a1b2c3d`). -5. CI-скрипт автоматически делает коммит в инфраструктурный репозиторий `infra`, заменяя старый тег образа на новый в файлах Kustomize. - -### **1.2. Этап CD (Доставка)** -ArgoCD (или аналогичный GitOps агент), запущенный внутри самого кластера Kubernetes, раз в минуту опрашивает репозиторий `infra`. Как только он видит новый коммит (обновился тег образа), он автоматически применяет эти изменения к кластеру, запуская Rolling Update. - ---- - -## **2. Управление Средами (Environments)** - -Проект `infra` структурирован с использованием **Kustomize** (встроен в kubectl). Это позволяет иметь общую базу (base) для всех сервисов и накладывать патчи (overlays) для конкретных сред. - -Структура директорий: -```text -infra/ -├── apps/ -│ ├── base/ -│ │ ├── auth-service/ (deployment.yaml, svc.yaml) -│ │ ├── chat-service/ -│ └── overlays/ -│ ├── dev/ (Меняет replicas: 1, ресурсы CPU/RAM) -│ ├── stage/ (Подключает тестовую БД) -│ └── prod/ (Меняет replicas: 10, подключает HPA, Production БД) -``` - ->[!NOTE] -> Среда `stage` (Staging) является точной зеркальной копией `prod` (по архитектуре, но не по мощностям). На нее направляется копия продакшен-трафика (Shadow Traffic) через Ingress Nginx для проверки сервисов под реальной нагрузкой перед релизом в прод. - ---- - -## **3. Стратегия релиза без простоев (Zero-Downtime Deployment)** - -Поскольку STREMO — это платформа реального времени, отключение API даже на 5 секунд приведет к разрыву стримов. Мы применяем стратегии постепенного выкатывания. - -### **3.1. Canary Deployments (Канареечные релизы)** - -Для критически важных компонентов (например, API Gateway или Auth Service) используется Canary-выкладка (через Argo Rollouts). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 20}}}%% -flowchart TD - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Ingress("Ingress Controller"):::solidBlack - - subgraph Deployment Target - direction LR - Stable("Stable Pods (v1.0)\n[90% Traffic]"):::storage - Canary("Canary Pods (v1.1)\n[10% Traffic]"):::dashedBlack - end - - Ingress -->|"Splits Traffic"| Stable - Ingress -->|"Splits Traffic"| Canary - - Metrics("Prometheus\n(Monitors Error Rate 5xx)"):::solidBlack -.->|"Checks Health"| Canary - - Metrics -->|"If OK: Promote to 100%\nIf Fail: Rollback to v1.0"| Argo("Argo Rollouts"):::solidBlack -``` - -**Процесс:** -1. ArgoCD запускает всего несколько подов с новой версией приложения (v1.1). -2. Ingress-контроллер перенаправляет на новые поды только 5-10% пользовательского трафика. -3. Система ждет 5 минут и анализирует метрики в Prometheus. Если количество ошибок HTTP 5xx на новых подах не превышает норму, трафик увеличивается до 50%, а затем до 100%. -4. Старые поды (v1.0) уничтожаются. - ->[!CAUTION] -> **Ограничение для WebSocket** -> Канареечные релизы сложны для `chat-service` (WebSockets). Существующие WS-соединения не могут быть плавно перенесены. Для чат-подов используется стратегия медленного Rolling Update: поды убиваются по одному. Клиент (браузер) замечает разрыв, использует экспоненциальную задержку (Exponential Backoff) и переподключается уже к новому, обновленному поду. diff --git a/docs/Sharding.md b/docs/Sharding.md deleted file mode 100644 index 98cc14c..0000000 --- a/docs/Sharding.md +++ /dev/null @@ -1,119 +0,0 @@ -# Стратегия Шардирования (Sharding Strategy) - -![](../img/sharding-md.png) - ->[!IMPORTANT] -> В высоконагруженных стриминговых системах объем генерируемых данных (особенно в чатах трансляций) мгновенно превышает возможности вертикального масштабирования (Scale-Up). Данный документ описывает механизмы горизонтального масштабирования (Scale-Out) с использованием кластера **PostgreSQL + Citus Data**. - ---- - -## **1. Архитектура Распределенной Базы Данных Citus** - -Citus превращает PostgreSQL в распределенную СУБД, состоящую из одного Coordinator-узла и множества Worker-узлов. Приложение (наши микросервисы) общается только с Coordinator-ом, думая, что работает с обычной локальной базой Postgres. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 100, "nodeSpacing": 30}}}%% -flowchart TD - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - API("Microservices\n(Chat, VOD, Stream Meta)"):::dashedBlack - - subgraph Citus Cluster - Coordinator("Coordinator Node\n(Routing & Planning)"):::solidBlack - - Worker1("Worker Node 1\n(Shards: 1001-1050)"):::storage - Worker2("Worker Node 2\n(Shards: 1051-1100)"):::storage - Worker3("Worker Node N\n(Shards: 1101-1150)"):::storage - end - - API -->|"Standard SQL Queries"| Coordinator - - Coordinator ==>|"Distributed Query Execution"| Worker1 - Coordinator ==>|"Distributed Query Execution"| Worker2 - Coordinator ==>|"Distributed Query Execution"| Worker3 -``` - -### **1.1. Маршрутизация запросов** -Когда `chat-service` отправляет запрос `SELECT * FROM chat_messages WHERE channel_id = 'user-123'`, Coordinator вычисляет хэш от `user-123`, определяет, на каком Worker-узле находится нужный шард, и прозрачно проксирует запрос именно туда, минуя остальные сервера. - ---- - -## **2. Выбор Ключа Распределения (Sharding Key)** - -Фундаментальное правило шардирования: **данные, которые запрашиваются вместе, должны лежать вместе (на одном физическом диске)**. - -Мы используем `channel_id` (ID стримера) в качестве единого ключа распределения (Distribution Column) для всех высоконагруженных таблиц (чаты, статистика, клипы). Это позволяет Citus выполнять операции `JOIN` локально на Worker-узлах без необходимости пересылать терабайты данных по сети (Network Shuffle). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 15}}}%% -flowchart LR - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - subgraph Logical Concept: Tenant Isolation - direction TB - TenantA("Channel A (channel_id=123)"):::solidBlack - TenantB("Channel B (channel_id=456)"):::solidBlack - end - - subgraph Physical Disk: Worker 1 - direction TB - DataA("chat_messages (ch_id=123)\nstreams (ch_id=123)\nvods (ch_id=123)"):::storage - end - - subgraph Physical Disk: Worker 2 - direction TB - DataB("chat_messages (ch_id=456)\nstreams (ch_id=456)\nvods (ch_id=456)"):::storage - end - - TenantA -->|"Hashes to Shard X"| DataA - TenantB -->|"Hashes to Shard Y"| DataB -``` - ->[!TIP] -> **Co-location (Совместное размещение)** -> Запрос вида `SELECT c.text, s.title FROM chat_messages c JOIN streams s ON c.channel_id = s.channel_id WHERE c.channel_id = '123'` выполнится за миллисекунды, так как обе таблицы для этого канала физически лежат на `Worker 1`. - ---- - -## **3. Reference Tables (Справочные таблицы)** - -Некоторые таблицы (например, `categories` со списком игр или `badges` со значками модераторов) нужны часто, но они слишком малы для шардирования по ключу. - -В Citus они помечаются как **Reference Tables**. Координатор автоматически создает полную копию (реплику) таких таблиц на **каждом** Worker-узле. Это позволяет делать JOIN шардированных таблиц (чат) со справочными (значки) без сетевых задержек. - ---- - -## **4. Решение проблемы Hotspots (Горячие точки)** - -Самая опасная ситуация при шардировании по `channel_id` — запуск стрима топовым киберспортсменом с онлайном 300,000 зрителей. В этот момент один конкретный `channel_id` сгенерирует 90% нагрузки всей платформы, и Worker, на котором лежит его шард, может упасть (Hotspot). - -### **4.1. Смягчение нагрузки (Mitigation Strategies)** - -1. **Batching на уровне приложения:** `chat-service` не пишет сообщения в БД поштучно. Он аккумулирует их в RAM и делает один Bulk Insert раз в 500мс. Даже 10,000 сообщений превратятся всего в 2 запроса к Worker-узлу. -2. **Tenant Isolation (Изоляция тенанта):** С помощью функции `citus_isolate_tenant()` мы можем на лету "вырезать" шард крупного стримера из общего Worker-узла и перенести его на отдельный, выделенный сверхмощный сервер без остановки записи. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 20}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - subgraph Before Isolation - WorkerA("Worker 1\n(Channels: A, B, C)"):::storage - end - - Admin("DBA / Automation"):::solidBlack -->|"citus_isolate_tenant('channel_A')"| Citus - - subgraph After Isolation - WorkerA_New("Worker 1\n(Channels: B, C)"):::storage - Worker_Dedicated("Worker 2 (Dedicated)\n(Channel A)"):::dashedBlack - end -``` - ->[!CAUTION] -> **Мониторинг шардов** -> SRE команда обязана настроить алерты в Grafana на CPU/IOPS каждого отдельного Citus Worker. Если утилизация диска (IO Wait) на одном воркере превышает 80% в течение 2 минут, запускается скрипт автоматической изоляции горячего шарда. diff --git a/docs/User.md b/docs/User.md deleted file mode 100644 index 72af049..0000000 --- a/docs/User.md +++ /dev/null @@ -1,118 +0,0 @@ -# Жизненный Цикл Пользователя и Ролевая Модель - -![](../img/user-md.png) - ->[!IMPORTANT] -> Данный документ описывает абстракцию "Пользователь" (User) с точки зрения архитектуры платформы. Он включает диаграмму состояний учетной записи (State Machine), сессионный менеджмент и ролевую модель доступа (RBAC). - ---- - -## **1. Модель Состояний (State Machine)** - -Пользователь в системе не статичен. В зависимости от уровня верификации и нарушений правил, его аккаунт переходит между жестко заданными состояниями. Маршрутизация на API Gateway (BFF) проверяет текущее состояние пользователя при каждом запросе. - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn"}}%% -stateDiagram-v2 - classDef default fill:#fff,stroke:#000,stroke-width:2px,color:#000 - classDef danger fill:#fff,stroke:#d63031,stroke-width:2px,stroke-dasharray: 6 6,color:#d63031,rx:15,ry:15 - - [*] --> Anonymous : Вход на сайт - Anonymous --> Unverified : Регистрация (POST /register) - - Unverified --> Verified : Подтверждение Email / Mobile ID - Verified --> Affiliate : Выполнение условий платформы - Affiliate --> Partner : Контракт со STREMO - - Verified --> Banned : Нарушение правил (Модерация) - Affiliate --> Banned : Нарушение правил - Partner --> Banned : Разрыв контракта / Нарушение - - Banned --> Verified : Апелляция одобрена - Banned --> Deleted : Перманентный бан (Hard Delete) - - Verified --> Deleted : Пользователь удалил аккаунт - Deleted --> [*] -``` - -### **Описание состояний:** -* **Anonymous:** Гость. Имеет доступ к чтению каталога (`GET /streams/live`) и просмотру трансляций. Может читать чат через WS, но не может писать. -* **Unverified:** Зарегистрированный аккаунт без подтвержденного Email/Телефона. Запрещен запуск трансляций и совершение покупок (защита от бот-сетей). -* **Verified:** Полноценный пользователь. Может донатить, писать в чат и запускать базовые стримы без возможности монетизации (без рекламы). -* **Affiliate / Partner:** Статусы стримеров. Открывают доступ к выплатам, кнопке подписки на канал (Tier 1/2/3) и кастомным смайликам. -* **Banned:** Доступ к API заблокирован на уровне Ingress (код `403 Forbidden`). При попытке входа или обновления токена запрос отклоняется. - ---- - -## **2. Ролевая Модель Контроля Доступа (RBAC)** - -Авторизация реализована на основе ролей (Role-Based Access Control). Роли вшиваются в полезную нагрузку (Payload) JWT-токена при входе в систему (вызов `POST /login`). - -### **2.1. Иерархия Ролей** - -| Роль (Role) | Глобальные права | Локальные права (Channel Scope) | -| :--- | :--- | :--- | -| `user` | Базовый доступ. | Нет. | -| `moderator` | Нет глобальных прав. | Имеет право выдавать таймауты и баны (`POST /ban`) только в чате того стримера, который назначил его модератором. | -| `admin` | Чтение логов, блокировка пользователей. | Может управлять любым каналом. | -| `superadmin` | Изменение конфигураций платформы. | Полный доступ. | - -### **2.2. JWT и Scope-базированая Авторизация** - -Чтобы микросервисы (например, `chat-service`) не делали запрос в БД для проверки прав при каждом сообщении пользователя, вся информация криптографически подписана внутри JWT токена. - -Пример расшифрованного Payload: -```json -{ - "sub": "usr-12345", - "username": "viewer99", - "status": "verified", - "global_roles": ["user"], - "channel_roles": { - "channel_id_789": ["moderator"], - "channel_id_101": ["vip"] - }, - "exp": 1716249600 -} -``` - ->[!TIP] -> **Проверка прав в Микросервисах** -> Когда пользователь отправляет запрос на бан другого зрителя (эндпоинт `/moderation/channels/789/ban`), микросервис Модерации просто извлекает JWT заголовок, парсит его (проверяя подпись) и смотрит, есть ли у пользователя роль `moderator` в массиве `channel_roles["channel_id_789"]`. Дополнительного похода в БД не требуется. - ---- - -## **3. Менеджмент Сессий и Безопасность** - -Платформа использует паттерн **Short-Lived Access Token + Long-Lived Refresh Token**. - -1. **Access Token (JWT):** Живет всего 15 минут. Хранится в памяти браузера фронтенда (не в LocalStorage, чтобы избежать XSS атак). Отправляется в каждом запросе в заголовке `Authorization: Bearer`. -2. **Refresh Token:** Живет 30 дней. Хранится в браузере в виде `HttpOnly; Secure; SameSite=Strict` Cookie. Недоступен для JavaScript. - -### **3.1. Отзыв сессий (Token Revocation & Blacklisting)** - -Классическая проблема JWT — невозможность отозвать токен до истечения его срока жизни (15 минут). Если пользователь меняет пароль или его банит администратор, злоумышленник может использовать украденный Access Token еще 15 минут. - -**Решение STREMO (Redis Blacklist):** -* При сбросе пароля или бане, `user_id` пользователя помещается в Redis с ключом `blacklist:usr-12345` и TTL = 15 минут. -* API Gateway (BFF) перед пересылкой запроса внутренним сервисам быстро (за 1-2 мс) проверяет наличие пользователя в Redis Blacklist. Если он там есть — запрос отклоняется с `401 Unauthorized`, заставляя клиента запросить новый токен (который не выдадут из-за измененного пароля/бана). - -```mermaid -%%{init: {"theme": "base", "look": "handDrawn", "flowchart": {"rankSpacing": 80, "nodeSpacing": 15}}}%% -flowchart LR - classDef dashedBlack fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 6 6,color:#000,rx:15,ry:15 - classDef solidBlack fill:#fff,stroke:#000,stroke-width:2px,color:#000,rx:15,ry:15 - classDef storage fill:#fdfdfd,stroke:#555,stroke-width:2px,color:#000,rx:5,ry:5 - - Client("Web Client"):::solidBlack -->|"1. API Request + JWT"| Gateway("API Gateway\n(Token Introspection)"):::dashedBlack - - Gateway -->|"2. Check Revoked?"| Redis("Redis\n(Blacklist Cache)"):::storage - - Redis -.->|"3. Not Revoked"| Gateway - - Gateway ==>|"4. Proxy Request"| TargetSvc("Target Microservice\n(e.g., Billing)"):::solidBlack -``` - ->[!CAUTION] -> **Безопасность Транзакций** -> Любые операции по списанию средств (донат, подписка) требуют обязательной валидации сессии на уровне `billing-service`. Даже если JWT прошел валидацию на Gateway, биллинг повторно проверит целостность аккаунта в глобальной БД перед транзакцией. diff --git a/docs/srs/Terms-of-Reference-STREMO.tex b/docs/srs/Terms-of-Reference-STREMO.tex deleted file mode 100644 index 561ad3f..0000000 --- a/docs/srs/Terms-of-Reference-STREMO.tex +++ /dev/null @@ -1,107 +0,0 @@ -\documentclass[12pt,a4paper]{article} - -\usepackage{fontspec} -\setmainfont{CMU Serif} -\setsansfont{CMU Sans Serif} -\setmonofont{CMU Typewriter Text} -\usepackage[russian]{babel} - -\usepackage{amsmath} -\usepackage{amssymb} -\usepackage{amsfonts} - -\usepackage{graphicx} -\usepackage{geometry} -\geometry{top=3cm, bottom=2.5cm, left=3cm, right=2cm, headheight=1.5cm, headsep=0.8cm} - -\usepackage{fancyhdr} -\pagestyle{fancy} -\fancyhf{} - -\fancyhead[L]{\includegraphics[height=1.2cm]{../img/logo.png}} -\fancyhead[R]{\small\scshape STREMO: Математические модели} -\fancyfoot[C]{\thepage} -\renewcommand{\headrulewidth}{0.4pt} -\renewcommand{\footrulewidth}{0pt} - -\usepackage{titlesec} -\titleformat{\section}{\Large\bfseries}{\thesection.}{0.5em}{} -\titleformat{\subsection}{\large\bfseries}{\thesubsection.}{0.5em}{} - -\begin{document} - -\thispagestyle{fancy} - -\begin{center} - \vspace{0.4cm} - \Large{\textbf{Спецификация математических моделей и алгоритмов обработки}}\\ - \vspace{0.5cm} - \rule{\textwidth}{0.4pt} -\end{center} - -\vspace{1cm} - -\section{1. Оптимизация размера медиа-чанка (Streaming Pipeline)} - -Для балансировки задержки `Latency` и пропускной способности `Bandwidth` при передаче живого видеопотока по протоколу gRPC необходимо аналитически определить оптимальный размер медиа-чанка $S_{chunk}$ в битах. - -Введем обозначения: -\begin{itemize} - \item $B_{network}$ --- доступная пропускная способность канала связи (бит/с). - \item $R_{bitrate}$ --- битрейт видеопотока (бит/с), при условии $B_{network} > R_{bitrate}$. - \item $T_{overhead}$ --- константная задержка на сериализацию Protobuf и отправку сетевого кадра (с). -\end{itemize} - -Длительность контента, содержащегося в одном чанке, вычисляется как $$D_{chunk} = \frac{S_{chunk}}{R_{bitrate}}$$.\\ -Время физической передачи данного чанка по сети составляет $$T_{tx} = \frac{S_{chunk}}{B_{network}} + T_{overhead}$$. - -Фундаментальное условие непрерывности трансляции (исключение опустошения буфера) требует, чтобы время передачи чанка не превышало его медийную длительность: -\begin{equation} -T_{tx} \le D_{chunk} \implies \frac{S_{chunk}}{B_{network}} + T_{overhead} \le \frac{S_{chunk}}{R_{bitrate}} -\end{equation} - -Решая данное неравенство относительно искомого размера чанка $S_{chunk}$, получаем: -\begin{equation} -S_{chunk} \left( \frac{1}{R_{bitrate}} - \frac{1}{B_{network}} \right) \ge T_{overhead} -\end{equation} -\begin{equation} -S_{chunk} \ge \frac{T_{overhead} \cdot R_{bitrate} \cdot B_{network}}{B_{network} - R_{bitrate}} -\end{equation} - -Поскольку end-to-end задержка прямо пропорциональна размеру чанка ($L \approx D_{chunk} + T_{tx}$), для ее минимизации размер чанка фиксируется на строгой нижней границе: -\begin{equation} -S_{opt} = \frac{T_{overhead} \cdot R_{bitrate} \cdot B_{network}}{B_{network} - R_{bitrate}} -\end{equation} - -\section{2. Динамическая модель буферизации (ABR)} - -Размер видеобуфера $Q(t)$ на стороне клиента в момент времени $t$ описывается обыкновенным дифференциальным уравнением: -\begin{equation} -\frac{dQ(t)}{dt} = \frac{C(t)}{R_v(t)} - 1 -\end{equation} -где $C(t)$ --- текущая пропускная способность сети, а $R_v(t)$ --- битрейт выбранного слоя качества. Целевая функция алгоритма адаптивного битрейта (ABR) сводится к максимизации качества при жестком ограничении вероятности опустошения буфера: -\begin{equation} -\max_{R_v} \sum_{k=1}^{K} U(R_v(k)) - \lambda \sum_{k=1}^{K-1} |R_v(k+1) - R_v(k)| -\end{equation} -при условии $Q(t) > 0$, где $U$ --- функция полезности, а $\lambda$ --- весовой коэффициент штрафа за переключение качества. - -\section{3. Матричные преобразования цветового пространства} - -В процессе транскодирования видеопотока (переход YUV $\leftrightarrow$ RGB) применяется линейное преобразование цветовых метрик. В рамках математического аппарата системы STREMO вектор состояния пикселя $\mathbf{v}$ строго определяется как \textbf{вектор-столбец}: -\begin{equation} -\mathbf{v} = \begin{pmatrix} v_1 \\ v_2 \\ v_3 \end{pmatrix} -\end{equation} - -Прямое преобразование из исходного цветового пространства задается операцией левостороннего умножения на матрицу преобразования $A$: -\begin{equation} -\mathbf{c} = A \mathbf{v} -\end{equation} - -Для обратного вычисления исходных компонент применяется левостороннее умножение вектора-столбца $\mathbf{c}$ на обратную матрицу $A^{-1}$: -\begin{equation} -\mathbf{v} = A^{-1} \mathbf{c} -\end{equation} - -Обратите внимание: операция транспонирования обратной матрицы в данном случае \textbf{не применяется}. Транспонирование необходимо исключительно при использовании конвенции векторов-строк (где $\mathbf{v}^T = \mathbf{c}^T (A^{-1})^T$). Так как система оперирует исключительно векторами-столбцами, форма $\mathbf{v} = A^{-1} \mathbf{c}$ сохраняет полную математическую корректность. - -\end{document} \ No newline at end of file diff --git a/docs/srs/Terms_of_Reference_STREMO.pdf b/docs/srs/Terms_of_Reference_STREMO.pdf deleted file mode 100644 index c0e0747..0000000 Binary files a/docs/srs/Terms_of_Reference_STREMO.pdf and /dev/null differ diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 5ef6a52..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index e215bc4..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,36 +0,0 @@ -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/eslint.config.mjs b/frontend/eslint.config.mjs deleted file mode 100644 index 05e726d..0000000 --- a/frontend/eslint.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), -]); - -export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts deleted file mode 100644 index 0b1ec0b..0000000 --- a/frontend/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - reactCompiler: true, -}; - -export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 024e862..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,6714 +0,0 @@ -{ - "name": "frontend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.1.0", - "dependencies": { - "lucide-react": "^1.14.0", - "next": "16.2.5", - "react": "19.2.4", - "react-dom": "19.2.4" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "babel-plugin-react-compiler": "1.0.0", - "eslint": "^9", - "eslint-config-next": "16.2.5", - "tailwindcss": "^4", - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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/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==", - "dev": true, - "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-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==", - "dev": true, - "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-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "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==", - "dev": true, - "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-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==", - "devOptional": true, - "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==", - "devOptional": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.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==", - "dev": true, - "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==", - "dev": true, - "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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.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==", - "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/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "musl" - ], - "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" - ], - "libc": [ - "musl" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "glibc" - ], - "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" - ], - "libc": [ - "musl" - ], - "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" - ], - "libc": [ - "musl" - ], - "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/@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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@next/env": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.5.tgz", - "integrity": "sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.5.tgz", - "integrity": "sha512-PyILm/cw2u5gEG5xOjqFbALUAl/erAqtM47iZtP9lXiSzin+eOIf3KRi+CBC/mFG9j7Iz3JDqCOY94nFLUCccg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.5.tgz", - "integrity": "sha512-BW+8PGVmsruomXHsitD8JG6gny9lEdobctjBwvtPF8AKtxGDR7nR35FOl/oK9UAPXBOBm+vx0k8qtpeHOXQMGQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.5.tgz", - "integrity": "sha512-ZoCGnCl9LlQJWmqXrZAUlNxvuNmclvE+7zUif+nDydkkehl9FKxHJ+wxSQMj+C37BYFerKiEdX9s9o02ir975Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.5.tgz", - "integrity": "sha512-AwcZzMChaWkOTZt3vu+2ZMIj8g4dYQY+B8VUVhlFSQ2JtvyZpefyYHTe00D6b6L7BysYw7vl3zsvs9jix8tl5Q==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.5.tgz", - "integrity": "sha512-QqMgqWbCBFsfiQ7BF3dUlW8HJy1LWhpcqbTpoHMWA9IV+TnWwDKozQJA5NdIAHjQ00yX2Q7AUkLr/XK4n77q8A==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.5.tgz", - "integrity": "sha512-3hzeiFGZtyATVx9pCeuzTshXmh50vHZitqaeZiyJZaUmjQyrfjsVUgS8apOj1vEJCIpKJM/55F45yPAV2kpjsA==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.5.tgz", - "integrity": "sha512-0mzZV/mAt7Qj2tYNdTB6AqrS8dwng/AQLSYC5Z1YLpZdi2wxqKDPK7RY2RvjB1fXyJfOfdA3l/yTF5yLi+WfuQ==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.5.tgz", - "integrity": "sha512-f/H4nZ2zJBvA8/+HpsB9mNonF9zfQoAU6D0WxJrfzhJDvJLfngVN85oqxUyrDVK99DIFfFYhLpGa5K+c5uotSw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.5.tgz", - "integrity": "sha512-nuP7DHs4koAojsIxVPkihNgKiRUKtCU65j5X6DAbSy8VBrfT/o90bCLLHPf51JEdOZwZMFzM6e0NiGWfIWjVAg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@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/@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/@swc/helpers": { - "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": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.4.tgz", - "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "postcss": "^8.5.6", - "tailwindcss": "4.2.4" - } - }, - "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/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/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.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "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.2", - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" - }, - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "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.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "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/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" - }, - "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.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "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/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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/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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.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-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/babel-plugin-react-compiler": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", - "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.27", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", - "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001792", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", - "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", - "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/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/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/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/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-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==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "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/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-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/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "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/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.352", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz", - "integrity": "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==", - "dev": true, - "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/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-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.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/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/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-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": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-next": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.5.tgz", - "integrity": "sha512-fXEkugikngux1FBJ/Vop+52SLAMFjXZFXjyl/+HjGHngnXf8iIfqe3qdjcwN+40RBpSsCVhI04j0/ngEWL5Qng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "16.2.5", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-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": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/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/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/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/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/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-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/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/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-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-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-bun-module/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-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-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==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-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-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-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-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-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/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==", - "dev": true, - "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/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=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/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/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", - "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/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==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.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/next": { - "version": "16.2.5", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz", - "integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==", - "license": "MIT", - "dependencies": { - "@next/env": "16.2.5", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.5", - "@next/swc-darwin-x64": "16.2.5", - "@next/swc-linux-arm64-gnu": "16.2.5", - "@next/swc-linux-arm64-musl": "16.2.5", - "@next/swc-linux-x64-gnu": "16.2.5", - "@next/swc-linux-x64-musl": "16.2.5", - "@next/swc-win32-arm64-msvc": "16.2.5", - "@next/swc-win32-x64-msvc": "16.2.5", - "sharp": "^0.34.5" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.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": { - "@opentelemetry/api": { - "optional": true - }, - "@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/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-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-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.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/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/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-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/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/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.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "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/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/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/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "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/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/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "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==", - "dev": true, - "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/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/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/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/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/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/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/sharp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/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/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/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/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-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.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" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "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/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "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/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/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/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/tsconfig-paths/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/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/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/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" - }, - "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/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/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/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/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/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index c2d182d..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "frontend", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint" - }, - "dependencies": { - "lucide-react": "^1.14.0", - "next": "16.2.5", - "react": "19.2.4", - "react-dom": "19.2.4" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "babel-plugin-react-compiler": "1.0.0", - "eslint": "^9", - "eslint-config-next": "16.2.5", - "tailwindcss": "^4", - "typescript": "^5" - } -} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs deleted file mode 100644 index 61e3684..0000000 --- a/frontend/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/frontend/public/file.svg b/frontend/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/frontend/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/frontend/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/frontend/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/frontend/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/frontend/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/frontend/src/app/favicon.ico and /dev/null differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css deleted file mode 100644 index a57aeae..0000000 --- a/frontend/src/app/globals.css +++ /dev/null @@ -1,85 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #000000; - --foreground: #f4f4f5; - --card: #1c1d1f; - --accent-pink: #f20e89; - --accent-purple: #e473ff; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-pink-primary: var(--accent-pink); - --color-purple-primary: var(--accent-purple); -} - -body { - background: var(--background); - color: var(--foreground); -} - -/* Custom Scrollbar */ -.custom-scrollbar::-webkit-scrollbar { width: 6px; } -.custom-scrollbar::-webkit-scrollbar-track { background: transparent; } -.custom-scrollbar::-webkit-scrollbar-thumb { background: #27272a; border-radius: 10px; } -.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #D673A9; } - -/* Animations */ -@keyframes dropdownSlide { - from { opacity: 0; transform: translateY(-10px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.animate-dropdown { - animation: dropdownSlide 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; - transform-origin: top right; -} - -@keyframes cookiePopIn { - 0% { opacity: 0; transform: translate(-50%, 40px) scale(0.95); } - 60% { opacity: 1; transform: translate(-50%, -5px) scale(1.02); } - 100% { opacity: 1; transform: translate(-50%, 0) scale(1); } -} - -.animate-cookie-in { - animation: cookiePopIn 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; -} - -@keyframes cookieFadeOut { - from { opacity: 1; transform: translate(-50%, 0) scale(1); } - to { opacity: 0; transform: translate(-50%, 20px) scale(0.95); } -} - -.animate-cookie-out { - animation: cookieFadeOut 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; -} - -/* Stream Player Placeholder Animation */ -@keyframes pulse-bg { - 0%, 100% { background-color: #27272a; } - 50% { background-color: #3f3f46; } -} -.animate-pulse-bg { - animation: pulse-bg 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.animate-fadeIn { - animation: fadeIn 0.3s ease-out forwards; -} - -@keyframes slideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -.animate-slideUp { - animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; -} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx deleted file mode 100644 index 550b6c7..0000000 --- a/frontend/src/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from "next"; -import "./globals.css"; - -export const metadata: Metadata = { - title: "STREMO - Stream Everything", - description: "A professional streaming platform landing page.", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx deleted file mode 100644 index 48a291a..0000000 --- a/frontend/src/app/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import React, { useState, useMemo } from 'react'; -import { Language } from '@/lib/i18n'; -import { Streamer } from '@/lib/data'; -import { Header } from '@/components/layout/Header'; -import { Sidebar } from '@/components/layout/Sidebar'; -import { HeroBanner } from '@/components/features/home/HeroBanner'; -import { CategoryGrid } from '@/components/features/home/CategoryGrid'; -import { CookieBanner } from '@/components/ui/CookieBanner'; -import { StreamView } from '@/components/features/stream/StreamView'; -import { BrowseView } from '@/components/features/browse/BrowseView'; -import { FollowingView } from '@/components/features/following/FollowingView'; -import { SearchView } from '@/components/features/search/SearchView'; -import { DashboardView } from '@/components/features/dashboard/DashboardView'; -import { Footer } from '@/components/layout/Footer'; - -type Tab = 'home' | 'browse' | 'following' | 'stream' | 'search' | 'dashboard'; - -export default function Home() { - const [activeTab, setActiveTab] = useState('home'); - const [language, setLanguage] = useState('ru'); // Default to Russian - const [selectedStreamer, setSelectedStreamer] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - - const handleStreamClick = (streamer: Streamer) => { - setSelectedStreamer(streamer); - setActiveTab('stream'); - setSearchQuery(''); // Clear search when opening stream - }; - - const currentTab = useMemo(() => { - if (searchQuery.length > 0) return 'search'; - return activeTab; - }, [searchQuery, activeTab]); - - const renderContent = () => { - switch (currentTab) { - case 'stream': - return selectedStreamer ? ( - - ) : ( -
Выберите стрим для просмотра
- ); - case 'browse': - return ; - case 'following': - return ; - case 'dashboard': - return ; - case 'search': - return ; - case 'home': - default: - return ( -
- - -
- ); - } - }; - - return ( -
- -
- -
- - -
-
- {renderContent()} -
- {currentTab !== 'stream' &&
} -
- - -
-
- ); -} diff --git a/frontend/src/components/features/browse/BrowseView.tsx b/frontend/src/components/features/browse/BrowseView.tsx deleted file mode 100644 index d2d490c..0000000 --- a/frontend/src/components/features/browse/BrowseView.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import React from "react"; -import { DICT, Language } from "@/lib/i18n"; -import { BOTTOM_CATEGORIES } from "@/lib/data"; - -interface BrowseViewProps { - language: Language; -} - -export function BrowseView({ language }: BrowseViewProps) { - const t = DICT[language]; - - return ( -
-

{t.browse}

- -
- - -
- -
- {BOTTOM_CATEGORIES.map((category) => ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {category.title} -
-

- {category.title} -

-

- {category.viewers} -

-
- {category.tags.slice(0, 2).map((tag) => ( - - {tag} - - ))} -
-
- ))} -
-
- ); -} diff --git a/frontend/src/components/features/dashboard/DashboardView.tsx b/frontend/src/components/features/dashboard/DashboardView.tsx deleted file mode 100644 index d7a8ed1..0000000 --- a/frontend/src/components/features/dashboard/DashboardView.tsx +++ /dev/null @@ -1,450 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { - Settings, - Mic, - Video, - Activity, - MessageSquare, - Plus, - Edit, - Users, - BarChart, - Layers, - Radio, - Camera, - Volume2, - Monitor, - Zap, - Shield, - Image, - Cpu, - MoreVertical, - Send, - Heart, - Play, -} from "lucide-react"; -import { DICT, Language } from "@/lib/i18n"; - -interface DashboardViewProps { - language: Language; -} - -export function DashboardView({ language }: DashboardViewProps) { - const t = DICT[language]; - const [activeMenu, setActiveMenu] = useState("Settings"); - const [isLive, setIsLive] = useState(false); - - return ( -
- {/* Dashboard Sidebar */} - - - {/* Main Content Area */} -
- {/* Dashboard Header */} -
-
-
- - Session - - - {isLive ? "00:06:24" : "00:00:00"} - -
-
- - Viewers - - - {isLive ? "135" : "0"} - -
-
- - Followers - - - 678 - -
-
- -
- - - -
-
- - {/* Dynamic Content */} -
- {isLive ? ( -
- {/* Left: Stream Preview & Actions */} -
-
-
-

- Stream Preview -

-
- - - Live - -
-
-
-
-
-
- -
-

- Live Camera Feed -

-

- Built-in Camera -

-
-
-
- -
-
-

- Quick Actions -

-
- } - label="Edit Info" - /> - } - label="Manage Poll" - /> - } label="" /> -
-
-
-

- Activity Feed -

-
- - - -
-
-
-
- - {/* Right: Chat */} -
-
-

- Live Chat -

-
-
- - - - -
-
-
- - -
-
-
-
- ) : ( -
-

{activeMenu}

- -
- {/* Audio Controls */} -
-
- -

- Audio Controls -

-
-
- - -
-
- - {/* Stream Quality */} -
-
- -

- Stream Quality -

-
-
-
-

- Quality -

-
- 1080p (Full HD) - -
-
-
-

- Bitrate -

-
- - 2233 kbps - -
-
-
-
-
-
-
- - {/* Camera Settings */} -
-
- -

- Camera Settings -

-
-
- - - -
-
- - {/* Stream Preview (Settings) */} -
-
-

- Stream Preview -

-
-
-
-
- -

- Preview Off -

-
-
-
-
-
- )} -
-
-
- ); -} - -function DashboardMenuItem({ icon, label, active, onClick }: any) { - return ( - - ); -} - -function ControlSlider({ label, value }: any) { - return ( -
-
-

- {label} -

- {value} -
-
-
-
-
-
- ); -} - -function QuickActionButton({ icon, label }: any) { - return ( - - ); -} - -function ActivityItem({ name, action, time }: any) { - return ( -
-
- -
-
-

- {name} {action} -

-

{time}

-
-
- ); -} - -function ChatBubble({ name, text, color }: any) { - return ( -
-
- - {name}: - - {text} -
-
- ); -} diff --git a/frontend/src/components/features/following/FollowingView.tsx b/frontend/src/components/features/following/FollowingView.tsx deleted file mode 100644 index 0bb642f..0000000 --- a/frontend/src/components/features/following/FollowingView.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import React from "react"; -import { DICT, Language } from "@/lib/i18n"; -import { SIDEBAR_FOLLOWING, Streamer } from "@/lib/data"; - -interface FollowingViewProps { - language: Language; - onStreamClick: (streamer: Streamer) => void; -} - -export function FollowingView({ language, onStreamClick }: FollowingViewProps) { - const t = DICT[language]; - const liveStreamers = SIDEBAR_FOLLOWING.filter((s) => s.isLive); - const offlineStreamers = SIDEBAR_FOLLOWING.filter((s) => !s.isLive); - - return ( -
-

{t.following}

- -
-

- Live Channels - - Live - -

-
- {liveStreamers.map((streamer) => ( - onStreamClick(streamer)} - /> - ))} -
-
- -
-

- Offline Channels -

-
- {offlineStreamers.map((streamer) => ( -
onStreamClick(streamer)} - > - {/* eslint-disable-next-line @next/next/no-img-element */} - {streamer.name} - - {streamer.name} - -
- ))} -
-
-
- ); -} - -function StreamCard({ streamer, onClick }: any) { - return ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {streamer.name} -
- Live -
-
- {streamer.viewers} viewers -
-
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {streamer.name} -
-

- Chill Stream with {streamer.name} -

-

{streamer.name}

-

{streamer.game}

-
-
-
- ); -} diff --git a/frontend/src/components/features/home/CategoryGrid.tsx b/frontend/src/components/features/home/CategoryGrid.tsx deleted file mode 100644 index 9083a15..0000000 --- a/frontend/src/components/features/home/CategoryGrid.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import React from "react"; -import { DICT, Language } from "@/lib/i18n"; -import { BOTTOM_CATEGORIES } from "@/lib/data"; - -interface CategoryGridProps { - language: Language; -} - -export function CategoryGrid({ language }: CategoryGridProps) { - const t = DICT[language]; - - return ( - <> -
-

{t.categoriesLike}

- -
- -
- {BOTTOM_CATEGORIES.map((category) => ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {category.title} -
-
- - STREMO - -
-
- -

- {category.title} -

-

- {category.viewers} -

- -
- {category.tags.slice(0, 2).map((tag) => ( - - {tag} - - ))} -
-
- ))} -
- - ); -} diff --git a/frontend/src/components/features/home/HeroBanner.tsx b/frontend/src/components/features/home/HeroBanner.tsx deleted file mode 100644 index d932a5c..0000000 --- a/frontend/src/components/features/home/HeroBanner.tsx +++ /dev/null @@ -1,201 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { Play, Heart, Share } from "lucide-react"; -import { DICT, Language } from "@/lib/i18n"; -import { RECOMMENDATIONS, Streamer } from "@/lib/data"; - -interface HeroBannerProps { - language: Language; - onStreamClick: (streamer: any) => void; -} - -export function HeroBanner({ language, onStreamClick }: HeroBannerProps) { - const t = DICT[language]; - const [recIndex, setRecIndex] = useState(0); - - const handleWatchNow = () => { - const currentRec = RECOMMENDATIONS[recIndex]; - onStreamClick({ - id: currentRec.id, - name: currentRec.channel, - game: currentRec.game, - viewers: currentRec.viewers, - avatar: currentRec.avatar, - isLive: true, - title: currentRec.title, - }); - }; - - return ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - Hero Background -
-
-
- -
-
-

- THE WITCHER 3 -

-

- WILD HUNT -

-
- -
- {["RPG", "Adventure Game", "Action", "Open World"].map((tag) => ( - - {tag} - - ))} -
- -

- The Witcher 3: Wild Hunt -

-

- Explore a dark-fantasy open world as monster slayer Geralt of Rivia in - The Witcher 3: Wild Hunt, one of the most acclaimed role-playing games - of all time. -

- -
- -
- - -
-
-
- - {/* Recommendation Slider */} -
-
-

{t.recStreamers}

-
- - {RECOMMENDATIONS.map((_, idx) => ( - -
-
- - {/* Slider Track */} -
-
- {RECOMMENDATIONS.map((stream, idx) => { - const isActive = idx === recIndex; - return ( -
{ - setRecIndex(idx); - if (isActive) { - onStreamClick({ - id: stream.id, - name: stream.channel, - game: stream.game, - viewers: stream.viewers, - avatar: stream.avatar, - isLive: true, - title: stream.title, - }); - } - }} - className={`w-[var(--card-width)] shrink-0 flex flex-col gap-3 transition-opacity duration-500 cursor-pointer ${isActive ? "opacity-100" : "opacity-50 hover:opacity-80"}`} - > -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - - -
- LIVE -
-
- {stream.viewers} -
-
- -
-

- {stream.title} -

-
- {/* eslint-disable-next-line @next/next/no-img-element */} - -
- - {stream.channel} - - - {stream.game} - -
-
-
-
- ); - })} -
-
-
-
- ); -} diff --git a/frontend/src/components/features/search/SearchView.tsx b/frontend/src/components/features/search/SearchView.tsx deleted file mode 100644 index 5fb001f..0000000 --- a/frontend/src/components/features/search/SearchView.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import React from "react"; -import { DICT, Language } from "@/lib/i18n"; -import { - BOTTOM_CATEGORIES, - SIDEBAR_FOLLOWING, - SIDEBAR_POPULAR, - Streamer, -} from "@/lib/data"; -import { User, Hash, PlayCircle } from "lucide-react"; - -interface SearchViewProps { - query: string; - language: Language; - onStreamClick: (streamer: Streamer) => void; -} - -export function SearchView({ - query, - language, - onStreamClick, -}: SearchViewProps) { - const t = DICT[language]; - const lowerQuery = query.toLowerCase(); - - const filteredCategories = BOTTOM_CATEGORIES.filter( - (c) => - c.title.toLowerCase().includes(lowerQuery) || - c.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)), - ); - - const allStreamers = [...SIDEBAR_FOLLOWING, ...SIDEBAR_POPULAR]; - const filteredStreamers = allStreamers.filter( - (s, index, self) => - self.findIndex((t) => t.id === s.id) === index && - (s.name.toLowerCase().includes(lowerQuery) || - s.game.toLowerCase().includes(lowerQuery)), - ); - - return ( -
-
-
- -
-
-

- {language === "ru" ? "Результаты поиска" : "Search Results"} -

-

- {language === "ru" ? "Для запроса" : "For query"}:{" "} - "{query}" -

-
-
- - {filteredStreamers.length > 0 && ( -
-
- -

- {language === "ru" ? "Каналы" : "Channels"} -

-
-
- {filteredStreamers.map((streamer) => ( -
onStreamClick(streamer)} - > -
- {streamer.name} -
-
-
- - {streamer.name} - - - {streamer.game || "Offline"} - -
- {streamer.viewers}{" "} - {language === "ru" ? "зрителей" : "viewers"} -
-
-
- ))} -
-
- )} - - {filteredCategories.length > 0 && ( -
-
- -

- {language === "ru" ? "Категории" : "Categories"} -

-
-
- {filteredCategories.map((category) => ( -
-
- {category.title} -
-
-

- {category.title} -

-

- {category.viewers} -

-
- {category.tags.slice(0, 2).map((tag) => ( - - {tag} - - ))} -
-
- ))} -
-
- )} - - {filteredStreamers.length === 0 && filteredCategories.length === 0 && ( -
-
- -
- - No matches found for "{query}" - -

- Try different keywords or filters -

-
- )} -
- ); -} diff --git a/frontend/src/components/features/stream/StreamView.tsx b/frontend/src/components/features/stream/StreamView.tsx deleted file mode 100644 index ad74913..0000000 --- a/frontend/src/components/features/stream/StreamView.tsx +++ /dev/null @@ -1,431 +0,0 @@ -"use client"; - -import React, { useState, useEffect, useRef } from "react"; -import { - Heart, - Share, - Users, - MessageSquare, - Send, - Smile, - Gift, - Trophy, - ExternalLink, - Camera, - Bird, - Code, -} from "lucide-react"; -import { DICT, Language } from "@/lib/i18n"; -import { Streamer } from "@/lib/data"; - -interface StreamViewProps { - streamer: Streamer; - language: Language; -} - -interface ChatMsg { - id: number; - user: string; - text: string; - color: string; - isMod?: boolean; - isVip?: boolean; -} - -const STICKERS = ["🔥", "😂", "😮", "❤️", "👍", "🤡", "👑", "GG"]; - -export function StreamView({ streamer, language }: StreamViewProps) { - const t = DICT[language]; - const [messages, setMessages] = useState([ - { - id: 1, - user: "Alex_Pro", - text: "PogChamp! What a play!", - color: "#D673A9", - }, - { id: 2, user: "KillerBee", text: "Is this real life?", color: "#60a5fa" }, - { - id: 3, - user: "StreamMod", - text: "Welcome everyone! Please be respectful.", - color: "#4ade80", - isMod: true, - }, - { - id: 4, - user: "ZeldaFan", - text: "Anyone knows the song name?", - color: "#fbbf24", - isVip: true, - }, - ]); - const [inputText, setInputText] = useState(""); - const [showStickers, setShowStickers] = useState(false); - const [bonusPoints, setBonusPoints] = useState(1250); - const chatEndRef = useRef(null); - - const scrollToBottom = () => { - chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - useEffect(scrollToBottom, [messages]); - - const handleSendMessage = (e?: React.FormEvent) => { - e?.preventDefault(); - if (!inputText.trim()) return; - - const newMsg: ChatMsg = { - id: Date.now(), - user: "Dmitry_X", - text: inputText, - color: "#D673A9", - }; - - setMessages([...messages, newMsg]); - setInputText(""); - setBonusPoints((prev) => prev + 10); - }; - - const addSticker = (sticker: string) => { - setInputText((prev) => prev + " " + sticker); - setShowStickers(false); - }; - - return ( -
-
- {/* Player Area */} -
-
-
-
-
- - STREMO PLAYER - -
-
-
-
-
-
-
- -
-
- Live -
- -
-
-
-
-
-
-
-
-
-
1080p60
-
-
-
-
- -
- {/* Channel Info */} -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {streamer.name} -
-

- {streamer.title || `${streamer.name}'s Chill Stream`} -

-
- - {streamer.name} - - - • {streamer.game} - -
-
-
- -
-
- - {streamer.viewers} -
- - -
-
- - {/* About & Banners Section */} -
-
-
-

- {t.about} -

- {/*Replace on real ddtd in rq #2-#3*/} -

- Welcome to my channel! I'm {streamer.name}, a professional{" "} - {streamer.game} player. Don't forget to follow and join our - discord for giveaways and community games! Business: - contact@{streamer.name.toLowerCase()}.com -

-
-
- 1.2M - - {t.followers} - -
-
-
- 8.5k - - Subs - -
-
-
- -
-
-
-
- -
- - Merch Store - - - 20% OFF NOW - -
-
-
-
-
- -
- - Instagram - - - @official_{streamer.name.toLowerCase()} - -
-
-
-
- -
-
-

- {t.links} -

-
- } - label="Twitter" - url="#" - /> - } - label="Instagram" - url="#" - /> - } - label="Discord" - url="#" - /> -
-
- -
-

- STREMO PLUS -

-

- Subscribe for exclusive emotes and ad-free viewing! -

- -
-
-
-
-
- - {/* Chat Area */} - -
-
- ); -} - -function SocialLink({ icon, label, url }: any) { - return ( - -
- - {icon} - - {label} -
- -
- ); -} - -function ChatMessage({ user, text, color, isMod, isVip }: any) { - return ( -
-
- {isMod && ( -
- -
- )} - {isVip && ( -
- -
- )} - - {user} - - {text} -
-
- ); -} diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx deleted file mode 100644 index f473d0b..0000000 --- a/frontend/src/components/layout/Footer.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import React from "react"; -import { DICT, Language } from "@/lib/i18n"; -import { Bird, Camera, Code, Play } from "lucide-react"; - -interface FooterProps { - language: Language; -} - -export function Footer({ language }: FooterProps) { - const t = DICT[language]; - - return ( -
-
-
-
- - - - - STREMO - -
-

- {t.description} -

-
- } /> - } /> - } /> - } /> -
-
- -
-

- Platform -

-
    -
  • - About Us -
  • -
  • - Careers -
  • -
  • - Press -
  • -
  • - Blog -
  • -
-
- -
-

- Support -

-
    -
  • - Help Center -
  • -
  • - Safety Center -
  • -
  • - Community Guidelines -
  • -
  • - Ad Choices -
  • -
-
- -
-

- Legal -

-
    -
  • - Privacy Policy -
  • -
  • - Terms of Service -
  • -
  • - Cookie Policy -
  • -
  • - GDPR -
  • -
-
-
- -
-

{t.copyright}

-
- - English - - Русский - - Deutsch - - - Français - -
-
-
- ); -} - -function SocialIcon({ icon }: any) { - return ( -
- {icon} -
- ); -} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx deleted file mode 100644 index b9cc444..0000000 --- a/frontend/src/components/layout/Header.tsx +++ /dev/null @@ -1,287 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { - Home, - Copy, - User, - Search, - Bell, - MessageCircle, - Settings, - Globe, - LogOut, - ChevronLeft, - ChevronRight, - Check, -} from "lucide-react"; -import { DICT, Language } from "@/lib/i18n"; -import { SearchOverlay } from "./SearchOverlay"; -import { Streamer } from "@/lib/data"; - -interface HeaderProps { - activeTab: string; - setActiveTab: (tab: string) => void; - language: Language; - setLanguage: (lang: Language) => void; - searchQuery: string; - setSearchQuery: (query: string) => void; - onStreamClick: (streamer: Streamer) => void; -} - -export function Header({ - activeTab, - setActiveTab, - language, - setLanguage, - searchQuery, - setSearchQuery, - onStreamClick, -}: HeaderProps) { - const [isProfileOpen, setIsProfileOpen] = useState(false); - const [profileView, setProfileView] = useState<"main" | "lang">("main"); - const [isSearchOpen, setIsSearchOpen] = useState(false); - const t = DICT[language]; - - React.useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "/" && !isSearchOpen) { - e.preventDefault(); - setIsSearchOpen(true); - } - }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [isSearchOpen]); - - return ( -
-
-
setActiveTab("home")} - > - - - - - STREMO - -
- - -
- -
-
setIsSearchOpen(true)} - className="flex items-center w-full h-10 px-4 rounded-full bg-[#27272a]/60 border border-transparent hover:border-[#D673A9]/30 transition-all cursor-text group" - > - -
- {searchQuery || t.search} -
-
- / -
-
-
- - setIsSearchOpen(false)} - query={searchQuery} - setQuery={setSearchQuery} - language={language} - onStreamClick={(s) => { - onStreamClick(s); - setIsSearchOpen(false); - }} - /> - -
- - - - - -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Profile { - setIsProfileOpen(!isProfileOpen); - setProfileView("main"); - }} - className={`w-9 h-9 rounded-full cursor-pointer bg-white border-2 transition-colors ${isProfileOpen ? "border-[#D673A9]" : "border-transparent hover:border-[#D673A9]/50"}`} - /> - - {isProfileOpen && ( - <> -
setIsProfileOpen(false)} - >
-
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - Profile -
-

Dmitry_X

-
-
-

- {t.online} -

-
-
-
- - {profileView === "main" ? ( -
- } - label={t.profile} - /> - } - label={t.settings} - /> - } - label={t.language} - value={language === "ru" ? "Русский" : "English"} - onClick={() => setProfileView("lang")} - showArrow - /> -
- } - label={t.logout} - hoverClass="hover:bg-red-500/10 hover:text-red-500" - /> -
- ) : ( -
- - { - setLanguage(lang as Language); - setProfileView("main"); - }} - /> - { - setLanguage(lang as Language); - setProfileView("main"); - }} - /> -
- )} -
- - )} -
-
-
- ); -} - -function ProfileMenuItem({ - icon, - label, - value, - onClick, - showArrow, - hoverClass = "hover:bg-white/5 hover:text-white", -}: any) { - return ( -
-
- {icon} - {label} -
-
- {value && ( - - {value} - - )} - {showArrow && } -
-
- ); -} - -function LanguageOption({ label, code, active, onSelect }: any) { - return ( -
onSelect(code)} - className="flex items-center justify-between px-3 py-2.5 rounded-xl cursor-pointer text-[#a1a1aa] hover:bg-white/5 hover:text-white transition-colors" - > - - {label} - - {active && } -
- ); -} diff --git a/frontend/src/components/layout/SearchOverlay.tsx b/frontend/src/components/layout/SearchOverlay.tsx deleted file mode 100644 index 6915846..0000000 --- a/frontend/src/components/layout/SearchOverlay.tsx +++ /dev/null @@ -1,369 +0,0 @@ -"use client"; - -import React, { useEffect, useRef } from "react"; -import { - Search, - X, - TrendingUp, - User, - Play, - Hash, - Clock, - ArrowRight, -} from "lucide-react"; -import { DICT, Language } from "@/lib/i18n"; -import { - BOTTOM_CATEGORIES, - SIDEBAR_FOLLOWING, - SIDEBAR_POPULAR, - Streamer, -} from "@/lib/data"; - -interface SearchOverlayProps { - isOpen: boolean; - onClose: () => void; - query: string; - setQuery: (query: string) => void; - language: Language; - onStreamClick: (streamer: Streamer) => void; -} - -export function SearchOverlay({ - isOpen, - onClose, - query, - setQuery, - language, - onStreamClick, -}: SearchOverlayProps) { - const t = DICT[language]; - const inputRef = useRef(null); - - useEffect(() => { - if (isOpen) { - document.body.style.overflow = "hidden"; - setTimeout(() => inputRef.current?.focus(), 100); - } else { - document.body.style.overflow = "auto"; - } - }, [isOpen]); - - useEffect(() => { - const handleEsc = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - window.addEventListener("keydown", handleEsc); - return () => window.removeEventListener("keydown", handleEsc); - }, [onClose]); - - if (!isOpen) return null; - - const lowerQuery = query.toLowerCase(); - - const allStreamers = [...SIDEBAR_FOLLOWING, ...SIDEBAR_POPULAR]; - const filteredStreamers = allStreamers - .filter( - (s, index, self) => - self.findIndex((t) => t.id === s.id) === index && // Unique by id - (s.name.toLowerCase().includes(lowerQuery) || - s.game.toLowerCase().includes(lowerQuery)), - ) - .slice(0, 6); - - const filteredCategories = BOTTOM_CATEGORIES.filter( - (c) => - c.title.toLowerCase().includes(lowerQuery) || - c.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)), - ).slice(0, 8); - - const trendingCategories = BOTTOM_CATEGORIES.slice(0, 4); - const recentSearches = [ - language === "ru" ? "Киберспорт" : "Esports", - "Minecraft", - "ASMR", - language === "ru" ? "Разговоры" : "Just Chatting", - ]; - - return ( -
- {/* Search Header */} -
-
-
- - - - - STREMO - -
- -
- - setQuery(e.target.value)} - placeholder={t.search} - className="w-full bg-transparent outline-none text-xl font-bold text-white placeholder-[#55555a]" - /> - {query && ( - - )} -
- - -
-
- - {/* Results / Suggestions */} -
-
- {!query ? ( -
- {/* Left Column: Recent & Trending */} -
-
-

- {" "} - {language === "ru" ? "Недавние запросы" : "Recent Searches"} -

-
- {recentSearches.map((search) => ( - - ))} -
-
- -
-

- {" "} - {language === "ru" ? "Популярно сейчас" : "Trending Now"} -

-
- {[ - "Gaming", - "ASMR", - "Programming", - "Art", - "Music", - "Chatting", - ].map((tag) => ( - - ))} -
-
-
- - {/* Right Column: Suggested Content */} -
-
-
-

- {language === "ru" - ? "Популярные категории" - : "Top Categories"} -

- -
-
- {trendingCategories.map((cat) => ( -
setQuery(cat.title)} - > -
- {cat.title} -
-
- {cat.viewers.split(" ")[0]} -
-
-

- {cat.title} -

-

- {cat.tags[0]} -

-
- ))} -
-
- -
-

- {language === "ru" - ? "Рекомендуемые каналы" - : "Recommended Channels"} -

-
- {SIDEBAR_POPULAR.slice(0, 4).map((streamer) => ( -
onStreamClick(streamer)} - > -
- {streamer.name} -
- Live -
-
-
-

- {streamer.name} -

-

- {streamer.game} -

-

- {streamer.viewers}{" "} - {language === "ru" ? "зрителей" : "viewers"} -

-
-
- ))} -
-
-
-
- ) : ( -
- {filteredStreamers.length > 0 && ( -
-

- Channels -

-
- {filteredStreamers.map((s) => ( -
onStreamClick(s)} - > - {s.name} -
-

- {s.name} -

-

- {s.game} -

-
-
- - {s.viewers} - -
-
-
- ))} -
-
- )} - - {filteredCategories.length > 0 && ( -
-

- Categories -

-
- {filteredCategories.map((cat) => ( -
setQuery(cat.title)} - > -
- {cat.title} -
-

- {cat.title} -

-

- {cat.viewers.split(" ")[0]} -

-
- ))} -
-
- )} - - {filteredStreamers.length === 0 && - filteredCategories.length === 0 && ( -
-
- -
- - No results for "{query}" - -

- Try checking your spelling or use more general keywords. -

-
- )} -
- )} -
-
-
- ); -} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx deleted file mode 100644 index 2ea6728..0000000 --- a/frontend/src/components/layout/Sidebar.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import React from "react"; -import { DICT, Language } from "@/lib/i18n"; -import { - SIDEBAR_FOLLOWING, - SIDEBAR_POPULAR, - SIDEBAR_CATEGORIES, - Streamer, -} from "@/lib/data"; -import { Clock } from "lucide-react"; - -interface SidebarProps { - language: Language; - onStreamClick: (streamer: Streamer) => void; -} - -export function Sidebar({ language, onStreamClick }: SidebarProps) { - const t = DICT[language]; - - return ( - - ); -} - -function SidebarSection({ title, items, btnText, onClick, type }: any) { - return ( -
-

- {title} -

-
- {items.map((item: any) => ( -
(onClick ? onClick(item) : null)} - > -
- {type - {type === "streamer" && item.isLive && ( -
- )} -
-
-
- - {type === "streamer" ? item.name : item.title} - -
- {item.isLive && ( -
- )} - - {item.viewers} - -
-
- - {type === "streamer" ? item.game || "Offline" : item.desc} - -
-
- ))} -
- -
- ); -} diff --git a/frontend/src/components/ui/CookieBanner.tsx b/frontend/src/components/ui/CookieBanner.tsx deleted file mode 100644 index 08a9467..0000000 --- a/frontend/src/components/ui/CookieBanner.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { Cookie } from "lucide-react"; -import { DICT, Language } from "@/lib/i18n"; - -interface CookieBannerProps { - language: Language; -} - -export function CookieBanner({ language }: CookieBannerProps) { - const [showCookieBanner, setShowCookieBanner] = useState(true); - const [isCookieClosing, setIsCookieClosing] = useState(false); - - const t = DICT[language]; - - const handleCloseCookie = () => { - setIsCookieClosing(true); - setTimeout(() => setShowCookieBanner(false), 400); - }; - - if (!showCookieBanner) return null; - - return ( -
-
-
- -
-

- {t.cookieText} -

-
-
- - -
-
- ); -} diff --git a/frontend/src/lib/data.ts b/frontend/src/lib/data.ts deleted file mode 100644 index c7a6cac..0000000 --- a/frontend/src/lib/data.ts +++ /dev/null @@ -1,246 +0,0 @@ -export interface Streamer { - id: number; - name: string; - game: string; - viewers: string; - avatar: string; - isLive: boolean; - title?: string; - tags?: string[]; -} - -export interface Category { - id: number; - title: string; - viewers: string; - tags: string[]; - image: string; - desc?: string; -} - -export const SIDEBAR_FOLLOWING: Streamer[] = [ - { - id: 1, - name: "Clemovitch", - game: "Just Chatting", - viewers: "17.3K", - avatar: "https://i.pravatar.cc/150?u=1", - isLive: true, - }, - { - id: 2, - name: "Glorious_E", - game: "The Witcher 3: Wild Hunt", - viewers: "56.5K", - avatar: "https://i.pravatar.cc/150?u=2", - isLive: true, - }, - { - id: 3, - name: "Camy", - game: "Call of Duty: Warzone", - viewers: "1.3K", - avatar: "https://i.pravatar.cc/150?u=3", - isLive: true, - }, - { - id: 4, - name: "BLASTPremier", - game: "", - viewers: "Offline", - avatar: "https://i.pravatar.cc/150?u=4", - isLive: false, - }, - { - id: 5, - name: "fubgun", - game: "", - viewers: "Offline", - avatar: "https://i.pravatar.cc/150?u=5", - isLive: false, - }, -]; - -export const SIDEBAR_POPULAR: Streamer[] = [ - { - id: 6, - name: "KaiCenat", - game: "Just Chatting", - viewers: "56.5K", - avatar: "https://i.pravatar.cc/150?u=6", - isLive: true, - }, - { - id: 7, - name: "Jynxzi", - game: "I'm Only Sleeping", - viewers: "9.3K", - avatar: "https://i.pravatar.cc/150?u=7", - isLive: true, - }, - { - id: 8, - name: "IShowSpeed", - game: "IRL", - viewers: "12.4K", - avatar: "https://i.pravatar.cc/150?u=8", - isLive: true, - }, - { - id: 9, - name: "Caedrel", - game: "Hollow Knight", - viewers: "18.6K", - avatar: "https://i.pravatar.cc/150?u=9", - isLive: true, - }, - { - id: 10, - name: "faxuty", - game: "Fortnite", - viewers: "9.9K", - avatar: "https://i.pravatar.cc/150?u=10", - isLive: true, - }, -]; - -export const SIDEBAR_CATEGORIES: Category[] = [ - { - id: 1, - title: "Politics", - desc: "IRL", - viewers: "56.5K", - image: "https://picsum.photos/seed/pol/100/100", - tags: ["IRL"], - }, - { - id: 2, - title: "Just Chatting", - desc: "IRL", - viewers: "56.5K", - image: "https://picsum.photos/seed/chat/100/100", - tags: ["IRL"], - }, - { - id: 3, - title: "Counter-Strike", - desc: "FPS, Shooter", - viewers: "56.5K", - image: "https://picsum.photos/seed/csgo/100/100", - tags: ["FPS", "Shooter"], - }, - { - id: 4, - title: "Path of Exile 2", - desc: "RPG, Action", - viewers: "56.5K", - image: "https://picsum.photos/seed/poe/100/100", - tags: ["RPG", "Action"], - }, - { - id: 5, - title: "The Witcher 3: W...", - desc: "RPG", - viewers: "56.5K", - image: "https://picsum.photos/seed/witcher_icon/100/100", - tags: ["RPG"], - }, -]; - -export const RECOMMENDATIONS = [ - { - id: 1, - title: "Direction Kear Morhen | Marathon THE WITCHER 3 + DL...", - channel: "Glorious_E", - game: "The Witcher 3: Wild Hunt", - viewers: "56.5K viewers", - image: "https://picsum.photos/seed/witcher_stream/600/338", - avatar: "https://i.pravatar.cc/150?u=2", - }, - { - id: 2, - title: "⭐#1 RANKED POV⭐30% OFF AL...", - channel: "Camy", - game: "Call of Duty: Warzone", - viewers: "1.3K viewers", - image: "https://picsum.photos/seed/cod/600/338", - avatar: "https://i.pravatar.cc/150?u=3", - }, - { - id: 3, - title: "ROAD TO GLOBAL ELITE | CS2 CHILL STREAM", - channel: "s1mple_fan", - game: "Counter-Strike 2", - viewers: "24.1K viewers", - image: "https://picsum.photos/seed/cs2_stream/600/338", - avatar: "https://i.pravatar.cc/150?u=4", - }, - { - id: 4, - title: "Just chatting and reacting to new trailers", - channel: "Pokimane_fan", - game: "Just Chatting", - viewers: "32.8K viewers", - image: "https://picsum.photos/seed/chat_stream/600/338", - avatar: "https://i.pravatar.cc/150?u=5", - }, -]; - -export const BOTTOM_CATEGORIES: Category[] = [ - { - id: 1, - title: "Just Chatting", - viewers: "232K viewers", - tags: ["IRL", "Casual"], - image: "https://picsum.photos/seed/justchat/400/533", - }, - { - id: 2, - title: "IRL", - viewers: "142K viewers", - tags: ["IRL", "Adventure"], - image: "https://picsum.photos/seed/irl_cat/400/533", - }, - { - id: 3, - title: "Minecraft", - viewers: "67.9K viewers", - tags: ["Simulation"], - image: "https://picsum.photos/seed/mine/400/533", - }, - { - id: 4, - title: "Dota 2", - viewers: "59.7K viewers", - tags: ["Strategy", "MOBA"], - image: "https://picsum.photos/seed/dota2/400/533", - }, - { - id: 5, - title: "Counter-Strike", - viewers: "21.2K viewers", - tags: ["FPS", "Shooter"], - image: "https://picsum.photos/seed/cs2/400/533", - }, - { - id: 6, - title: "Path of Exile 2", - viewers: "128K viewers", - tags: ["Action", "RPG"], - image: "https://picsum.photos/seed/poe2/400/533", - }, - { - id: 7, - title: "Politics", - viewers: "45.1K viewers", - tags: ["IRL"], - image: "https://picsum.photos/seed/polcat/400/533", - }, - { - id: 8, - title: "The Witcher 3", - viewers: "32.4K viewers", - tags: ["RPG"], - image: "https://picsum.photos/seed/w3cat/400/533", - }, -]; diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts deleted file mode 100644 index e849a34..0000000 --- a/frontend/src/lib/i18n.ts +++ /dev/null @@ -1,76 +0,0 @@ -export type Language = "en" | "ru"; - -export const DICT = { - en: { - home: "Home", - browse: "Browse", - following: "Following", - search: "Search", - goLive: "Go Live", - cookieText: - "We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking 'Accept', you consent to our use of cookies.", - accept: "Accept", - decline: "Decline", - recStreamers: "Recommendation Streamers", - categoriesLike: "Categories we think you'll like", - viewAll: "View All", - watchNow: "Watch Now", - follow: "Follow", - share: "Share", - sidebarFollowing: "Following", - sidebarPopular: "Popular", - sidebarCategories: "Categories", - showMore: "Show More", - profile: "Profile", - settings: "Settings", - language: "Language", - logout: "Log Out", - back: "Back", - online: "Online", - about: "About", - followers: "Followers", - chatBonus: "Bonus", - send: "Send", - links: "Social Links", - partners: "Partners", - copyright: "© 2026 STREMO. All rights reserved.", - description: - "The best streaming platform for creators and viewers alike. Join the community and start your journey today.", - }, - ru: { - home: "Главная", - browse: "Обзор", - following: "Подписки", - search: "Поиск", - goLive: "В эфир", - cookieText: - "Мы используем файлы cookie для персонализации контента и улучшения пользовательского опыта. Нажимая «Принять», вы соглашаетесь с нашей политикой.", - accept: "Принять", - decline: "Отклонить", - recStreamers: "Рекомендуемые стримеры", - categoriesLike: "Категории для вас", - viewAll: "Все", - watchNow: "Смотреть", - follow: "Отслеживать", - share: "Поделиться", - sidebarFollowing: "Отслеживаемые", - sidebarPopular: "Популярное", - sidebarCategories: "Категории", - showMore: "Показать еще", - profile: "Мой канал", - settings: "Настройки", - language: "Язык", - logout: "Выйти", - back: "Назад", - online: "В сети", - about: "О канале", - followers: "Подписчиков", - chatBonus: "Бонус", - send: "Отправить", - links: "Соцсети", - partners: "Партнеры", - copyright: "© 2026 STREMO. Все права защищены.", - description: - "Лучшая стриминговая платформа для создателей и зрителей. Присоединяйтесь к сообществу и начните свой путь сегодня.", - }, -}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 151a220..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next", - }, - ], - "paths": { - "@/*": ["./src/*"], - }, - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts", - "**/*.mts", - ], - "exclude": ["node_modules"], -} diff --git a/img/Screenshot 2026-05-07 at 19.33.02.png b/img/Screenshot 2026-05-07 at 19.33.02.png deleted file mode 100644 index 7d2321e..0000000 Binary files a/img/Screenshot 2026-05-07 at 19.33.02.png and /dev/null differ diff --git a/img/api-md.png b/img/api-md.png deleted file mode 100644 index f74c274..0000000 Binary files a/img/api-md.png and /dev/null differ diff --git a/img/arch-md.png b/img/arch-md.png deleted file mode 100644 index 3f6c6ae..0000000 Binary files a/img/arch-md.png and /dev/null differ diff --git a/img/ci-cd-md.png b/img/ci-cd-md.png deleted file mode 100644 index 00f5b3c..0000000 Binary files a/img/ci-cd-md.png and /dev/null differ diff --git a/img/db-md.png b/img/db-md.png deleted file mode 100644 index e850fa1..0000000 Binary files a/img/db-md.png and /dev/null differ diff --git a/img/deploy-md.png b/img/deploy-md.png deleted file mode 100644 index 2965d1d..0000000 Binary files a/img/deploy-md.png and /dev/null differ diff --git a/img/image copy.png b/img/image copy.png deleted file mode 100644 index 732b3f2..0000000 Binary files a/img/image copy.png and /dev/null differ diff --git a/img/image.png b/img/image.png deleted file mode 100644 index e1387c7..0000000 Binary files a/img/image.png and /dev/null differ diff --git a/img/sharding-md.png b/img/sharding-md.png deleted file mode 100644 index d91a844..0000000 Binary files a/img/sharding-md.png and /dev/null differ diff --git a/img/title-readme.png b/img/title-readme.png deleted file mode 100644 index 8ef5651..0000000 Binary files a/img/title-readme.png and /dev/null differ diff --git a/img/user-md.png b/img/user-md.png deleted file mode 100644 index 631cd93..0000000 Binary files a/img/user-md.png and /dev/null differ diff --git a/infra/apps/base/bff/deployment.yaml b/infra/apps/base/bff/deployment.yaml deleted file mode 100644 index a9f734a..0000000 --- a/infra/apps/base/bff/deployment.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: bff-config - namespace: apps -data: - NODE_ENV: "production" - PORT: "3000" ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bff - namespace: apps -spec: - replicas: 1 - selector: - matchLabels: - app: bff - template: - metadata: - labels: - app: bff - spec: - containers: - - name: bff - image: nginx:alpine - ports: - - containerPort: 3000 - envFrom: - - configMapRef: - name: bff-config - resources: - requests: - memory: "64Mi" - cpu: "100m" - limits: - memory: "128Mi" - cpu: "250m" ---- -apiVersion: v1 -kind: Service -metadata: - name: bff-service - namespace: apps -spec: - ports: - - port: 3000 - targetPort: 3000 - selector: - app: bff diff --git a/infra/apps/base/ingest/deployment.yaml b/infra/apps/base/ingest/deployment.yaml deleted file mode 100644 index 2b0880e..0000000 --- a/infra/apps/base/ingest/deployment.yaml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: ingest-config - namespace: apps -data: - RTMP_PORT: "1935" - REDIS_HOST: "redis.data-layer.svc.cluster.local" - KAFKA_BROKERS: "redpanda.data-layer.svc.cluster.local:9092" ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ingest - namespace: apps -spec: - replicas: 1 - selector: - matchLabels: - app: ingest - template: - metadata: - labels: - app: ingest - spec: - containers: - - name: ingest - image: debian:bullseye-slim # потом поправить как нить - command: ["sleep", "infinity"] - ports: - - containerPort: 1935 - envFrom: - - configMapRef: - name: ingest-config - resources: - requests: - memory: "64Mi" - cpu: "100m" - limits: - memory: "128Mi" - cpu: "500m" ---- -apiVersion: v1 -kind: Service -metadata: - name: ingest-service - namespace: apps -spec: - type: LoadBalancer - ports: - - port: 1935 - targetPort: 1935 - protocol: TCP - selector: - app: ingest diff --git a/infra/apps/base/ingress/traefik-ingress.yaml b/infra/apps/base/ingress/traefik-ingress.yaml deleted file mode 100644 index 981d96f..0000000 --- a/infra/apps/base/ingress/traefik-ingress.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: api-ingress - namespace: apps -spec: - entryPoints: - - web - routes: - - match: PathPrefix(`/v1`) || PathPrefix(`/api`) - kind: Rule - services: - - name: bff-service - port: 3000 - - match: PathPrefix(`/ws`) - kind: Rule - services: - - name: chat-service - port: 8080 ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: monitoring-ingress - namespace: monitoring -spec: - entryPoints: - - web - routes: - - match: Host(`grafana.localhost`) - kind: Rule - services: - - name: grafana - port: 80 - - match: Host(`prometheus.localhost`) - kind: Rule - services: - - name: prometheus - port: 9090 diff --git a/infra/apps/base/kustomization.yaml b/infra/apps/base/kustomization.yaml deleted file mode 100644 index 9687d03..0000000 --- a/infra/apps/base/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ingress/traefik-ingress.yaml - - bff/deployment.yaml - - ingest/deployment.yaml diff --git a/infra/apps/overlays/dev/kustomization.yaml b/infra/apps/overlays/dev/kustomization.yaml deleted file mode 100644 index e47042c..0000000 --- a/infra/apps/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- -patches: - - target: - kind: Deployment - name: bff - patch: |- - - op: replace - path: /spec/template/spec/containers/0/envFrom/0/configMapRef/name - value: dev-bff-config - - target: - kind: ConfigMap - name: bff-config - patch: |- - - op: replace - path: /data/NODE_ENV - value: "development" diff --git a/infra/apps/overlays/prod/kustomization.yaml b/infra/apps/overlays/prod/kustomization.yaml deleted file mode 100644 index 9713d7d..0000000 --- a/infra/apps/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -patches: - - target: - kind: Deployment - name: bff - patch: |- - - op: replace - path: /spec/replicas - value: 5 - - op: replace - path: /spec/template/spec/containers/0/resources/limits/memory - value: "1Gi" - - op: replace - path: /spec/template/spec/containers/0/resources/limits/cpu - value: "1000m" diff --git a/infra/apps/overlays/stage/kustomization.yaml b/infra/apps/overlays/stage/kustomization.yaml deleted file mode 100644 index 7cf856d..0000000 --- a/infra/apps/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage- -patches: - - target: - kind: Deployment - name: bff - patch: |- - - op: replace - path: /spec/replicas - value: 2 - - op: replace - path: /spec/template/spec/containers/0/envFrom/0/configMapRef/name - value: stage-bff-config - - target: - kind: ConfigMap - name: bff-config - patch: |- - - op: replace - path: /data/NODE_ENV - value: "staging" diff --git a/infra/docker/prometheus/prometheus.yml b/infra/docker/prometheus/prometheus.yml deleted file mode 100644 index e7cdf31..0000000 --- a/infra/docker/prometheus/prometheus.yml +++ /dev/null @@ -1,17 +0,0 @@ -global: - scrape_interval: 15s - evaluation_interval: 15s - -scrape_configs: - - job_name: "prometheus" - static_configs: - - targets: ["localhost:9090"] - - - job_name: "redpanda" - static_configs: - - targets: ["redpanda:9644"] - - #! В будущем дописать, далеком будущем ! - # - job_name: 'cpp-microservices' - # static_configs: - # - targets: ['ingest:8081', 'transcoder:8081', 'chat:8081'] diff --git a/infra/environments/dev/kustomization.yaml b/infra/environments/dev/kustomization.yaml deleted file mode 100644 index fec4103..0000000 --- a/infra/environments/dev/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../../namespaces/overlays/dev - - ../../postgres/overlays/dev - - ../../redis/overlays/dev - - ../../kafka/overlays/dev - - ../../s3/overlays/dev - - ../../monitoring/overlays/dev - - ../../apps/overlays/dev diff --git a/infra/environments/prod/kustomization.yaml b/infra/environments/prod/kustomization.yaml deleted file mode 100644 index ccc081b..0000000 --- a/infra/environments/prod/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../../namespaces/overlays/prod - - ../../postgres/overlays/prod - - ../../redis/overlays/prod - - ../../kafka/overlays/prod - - ../../s3/overlays/prod - - ../../monitoring/overlays/prod - - ../../apps/overlays/prod diff --git a/infra/environments/stage/kustomization.yaml b/infra/environments/stage/kustomization.yaml deleted file mode 100644 index bac3682..0000000 --- a/infra/environments/stage/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../../namespaces/overlays/stage - - ../../postgres/overlays/stage - - ../../redis/overlays/stage - - ../../kafka/overlays/stage - - ../../s3/overlays/stage - - ../../monitoring/overlays/stage - - ../../apps/overlays/stage diff --git a/infra/kafka/base/kustomization.yaml b/infra/kafka/base/kustomization.yaml deleted file mode 100644 index 1d5091b..0000000 --- a/infra/kafka/base/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - redpanda.yaml diff --git a/infra/kafka/base/redpanda.yaml b/infra/kafka/base/redpanda.yaml deleted file mode 100644 index 7c6296f..0000000 --- a/infra/kafka/base/redpanda.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: redpanda - namespace: data-layer -spec: - serviceName: redpanda - replicas: 1 - selector: - matchLabels: - app: redpanda - template: - metadata: - labels: - app: redpanda - spec: - containers: - - name: redpanda - image: docker.redpanda.com/redpandadata/redpanda:v23.2.1 - command: - - /bin/sh - - -c - - | - /usr/bin/rpk redpanda start \ - --mode dev-container \ - --smp 1 \ - --memory 512M \ - --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092 \ - --advertise-kafka-addr internal://redpanda.data-layer.svc.cluster.local:9092,external://localhost:19092 - ports: - - containerPort: 9092 - - containerPort: 19092 - resources: - requests: - memory: "256Mi" - cpu: "200m" - limits: - memory: "768Mi" - cpu: "1000m" ---- -apiVersion: v1 -kind: Service -metadata: - name: redpanda - namespace: data-layer -spec: - ports: - - name: kafka-internal - port: 9092 - - name: kafka-external - port: 19092 - selector: - app: redpanda diff --git a/infra/kafka/overlays/dev/kustomization.yaml b/infra/kafka/overlays/dev/kustomization.yaml deleted file mode 100644 index 88dffbe..0000000 --- a/infra/kafka/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- diff --git a/infra/kafka/overlays/prod/kustomization.yaml b/infra/kafka/overlays/prod/kustomization.yaml deleted file mode 100644 index eb9d39f..0000000 --- a/infra/kafka/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -patches: - - target: - kind: StatefulSet - name: redpanda - patch: |- - - op: replace - path: /spec/replicas - value: 3 - - op: replace - path: /spec/template/spec/containers/0/resources/limits/memory - value: "4Gi" diff --git a/infra/kafka/overlays/stage/kustomization.yaml b/infra/kafka/overlays/stage/kustomization.yaml deleted file mode 100644 index 0ba21da..0000000 --- a/infra/kafka/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage- diff --git a/infra/monitoring/base/grafana/deployment.yaml b/infra/monitoring/base/grafana/deployment.yaml deleted file mode 100644 index fa5dfd6..0000000 --- a/infra/monitoring/base/grafana/deployment.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-datasources - namespace: monitoring -data: - prometheus.yaml: |- - apiVersion: 1 - datasources: - - name: Prometheus - type: prometheus - url: http://prometheus.monitoring.svc.cluster.local:9090 - access: proxy - isDefault: true ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-dashboards-provider - namespace: monitoring -data: - dashboards.yaml: |- - apiVersion: 1 - providers: - - name: 'default' - orgId: 1 - folder: '' - type: file - disableDeletion: false - editable: true - options: - path: /var/lib/grafana/dashboards ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: grafana - namespace: monitoring -spec: - replicas: 1 - selector: - matchLabels: - app: grafana - template: - metadata: - labels: - app: grafana - spec: - containers: - - name: grafana - image: grafana/grafana-oss:10.0.3 - ports: - - containerPort: 3000 - env: - - name: GF_SECURITY_ADMIN_PASSWORD - value: "admin" # в секретик потом! - - name: GF_USERS_ALLOW_SIGN_UP - value: "false" - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "256Mi" - cpu: "300m" - volumeMounts: - - name: grafana-datasources - mountPath: /etc/grafana/provisioning/datasources - - name: grafana-dashboards-provider - mountPath: /etc/grafana/provisioning/dashboards - volumes: - - name: grafana-datasources - configMap: - name: grafana-datasources - - name: grafana-dashboards-provider - configMap: - name: grafana-dashboards-provider ---- -apiVersion: v1 -kind: Service -metadata: - name: grafana - namespace: monitoring -spec: - ports: - - port: 80 - targetPort: 3000 - selector: - app: grafana diff --git a/infra/monitoring/base/kustomization.yaml b/infra/monitoring/base/kustomization.yaml deleted file mode 100644 index 466fee7..0000000 --- a/infra/monitoring/base/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - prometheus/deployment.yaml - - grafana/deployment.yaml diff --git a/infra/monitoring/base/prometheus/deployment.yaml b/infra/monitoring/base/prometheus/deployment.yaml deleted file mode 100644 index 2162fc2..0000000 --- a/infra/monitoring/base/prometheus/deployment.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: prometheus-config - namespace: monitoring -data: - prometheus.yml: |- - global: - scrape_interval: 15s # Чаще для live-платформы, но не 1s чтобы не убить RAM - evaluation_interval: 15s - scrape_configs: - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - # Scrape C++ Microservices - - job_name: 'cpp-microservices' - kubernetes_sd_configs: - - role: pod - relabel_configs: - - source_labels: [__meta_kubernetes_pod_label_app] - regex: 'ingest|transcoder|chat|bff|auth' - action: keep - - source_labels: [__meta_kubernetes_pod_container_port_name] - regex: 'metrics' - action: keep - # Scrape Kafka (Redpanda) - - job_name: 'redpanda' - static_configs: - - targets: ['redpanda.data-layer.svc.cluster.local:9644'] ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: prometheus - namespace: monitoring -spec: - replicas: 1 - selector: - matchLabels: - app: prometheus - template: - metadata: - labels: - app: prometheus - spec: - containers: - - name: prometheus - image: prom/prometheus:v2.45.0 - args: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--storage.tsdb.retention.time=1d" - - "--web.enable-lifecycle" - ports: - - containerPort: 9090 - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "256Mi" - cpu: "500m" - volumeMounts: - - name: prometheus-config - mountPath: /etc/prometheus/ - - name: prometheus-data - mountPath: /prometheus - volumes: - - name: prometheus-config - configMap: - name: prometheus-config - - name: prometheus-data - emptyDir: {} ---- -apiVersion: v1 -kind: Service -metadata: - name: prometheus - namespace: monitoring -spec: - ports: - - port: 9090 - targetPort: 9090 - selector: - app: prometheus ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: prometheus -rules: - - apiGroups: [""] - resources: ["nodes", "nodes/proxy", "services", "endpoints", "pods"] - verbs: ["get", "list", "watch"] - - apiGroups: ["extensions", "networking.k8s.io"] - resources: ["ingresses"] - verbs: ["get", "list", "watch"] - - nonResourceURLs: ["/metrics"] - verbs: ["get"] ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: prometheus - namespace: monitoring ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: prometheus -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: prometheus -subjects: - - kind: ServiceAccount - name: prometheus - namespace: monitoring diff --git a/infra/monitoring/overlays/dev/kustomization.yaml b/infra/monitoring/overlays/dev/kustomization.yaml deleted file mode 100644 index 88dffbe..0000000 --- a/infra/monitoring/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- diff --git a/infra/monitoring/overlays/prod/kustomization.yaml b/infra/monitoring/overlays/prod/kustomization.yaml deleted file mode 100644 index 2333422..0000000 --- a/infra/monitoring/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base diff --git a/infra/monitoring/overlays/stage/kustomization.yaml b/infra/monitoring/overlays/stage/kustomization.yaml deleted file mode 100644 index 0ba21da..0000000 --- a/infra/monitoring/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage- diff --git a/infra/namespaces/base/kustomization.yaml b/infra/namespaces/base/kustomization.yaml deleted file mode 100644 index a1eee58..0000000 --- a/infra/namespaces/base/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - namespaces.yaml diff --git a/infra/namespaces/base/namespaces.yaml b/infra/namespaces/base/namespaces.yaml deleted file mode 100644 index 00a3e8b..0000000 --- a/infra/namespaces/base/namespaces.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: data-layer - labels: - name: data-layer ---- -apiVersion: v1 -kind: Namespace -metadata: - name: apps - labels: - name: apps ---- -apiVersion: v1 -kind: Namespace -metadata: - name: monitoring - labels: - name: monitoring diff --git a/infra/namespaces/overlays/dev/kustomization.yaml b/infra/namespaces/overlays/dev/kustomization.yaml deleted file mode 100644 index 88dffbe..0000000 --- a/infra/namespaces/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- diff --git a/infra/namespaces/overlays/prod/kustomization.yaml b/infra/namespaces/overlays/prod/kustomization.yaml deleted file mode 100644 index 2333422..0000000 --- a/infra/namespaces/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base diff --git a/infra/namespaces/overlays/stage/kustomization.yaml b/infra/namespaces/overlays/stage/kustomization.yaml deleted file mode 100644 index 0ba21da..0000000 --- a/infra/namespaces/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage- diff --git a/infra/postgres/base/kustomization.yaml b/infra/postgres/base/kustomization.yaml deleted file mode 100644 index b9031ef..0000000 --- a/infra/postgres/base/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - statefulset.yaml diff --git a/infra/postgres/base/statefulset.yaml b/infra/postgres/base/statefulset.yaml deleted file mode 100644 index 3cb95e1..0000000 --- a/infra/postgres/base/statefulset.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: postgres - namespace: data-layer -spec: - serviceName: postgres - replicas: 1 - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - containers: - - name: postgres - image: postgres:16-alpine - env: - - name: POSTGRES_USER - value: "itter_admin" - - name: POSTGRES_PASSWORD - value: "secret_password" - - name: POSTGRES_DB - value: "itterstream" - ports: - - containerPort: 5432 - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "256Mi" - cpu: "500m" - volumeMounts: - - name: pgdata - mountPath: /var/lib/postgresql/data - volumeClaimTemplates: - - metadata: - name: pgdata - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 2Gi ---- -apiVersion: v1 -kind: Service -metadata: - name: postgres - namespace: data-layer -spec: - ports: - - port: 5432 - selector: - app: postgres diff --git a/infra/postgres/overlays/dev/kustomization.yaml b/infra/postgres/overlays/dev/kustomization.yaml deleted file mode 100644 index 88dffbe..0000000 --- a/infra/postgres/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- diff --git a/infra/postgres/overlays/prod/kustomization.yaml b/infra/postgres/overlays/prod/kustomization.yaml deleted file mode 100644 index f0d24e4..0000000 --- a/infra/postgres/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -patches: - - target: - kind: StatefulSet - name: postgres - patch: |- - - op: replace - path: /spec/replicas - value: 3 - - op: replace - path: /spec/template/spec/containers/0/resources/limits/memory - value: "4Gi" diff --git a/infra/postgres/overlays/stage/kustomization.yaml b/infra/postgres/overlays/stage/kustomization.yaml deleted file mode 100644 index 0ba21da..0000000 --- a/infra/postgres/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage- diff --git a/infra/redis/base/deployment.yaml b/infra/redis/base/deployment.yaml deleted file mode 100644 index 3407799..0000000 --- a/infra/redis/base/deployment.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis - namespace: data-layer -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: redis:7-alpine - command: ["redis-server", "--appendonly", "yes"] - ports: - - containerPort: 6379 - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "128Mi" - cpu: "200m" - volumeMounts: - - name: redis-data - mountPath: /data - volumes: - - name: redis-data - emptyDir: {} # доделать, я спать ---- -apiVersion: v1 -kind: Service -metadata: - name: redis - namespace: data-layer -spec: - ports: - - port: 6379 - selector: - app: redis diff --git a/infra/redis/base/kustomization.yaml b/infra/redis/base/kustomization.yaml deleted file mode 100644 index 9c2d28b..0000000 --- a/infra/redis/base/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - deployment.yaml diff --git a/infra/redis/overlays/dev/kustomization.yaml b/infra/redis/overlays/dev/kustomization.yaml deleted file mode 100644 index 88dffbe..0000000 --- a/infra/redis/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- diff --git a/infra/redis/overlays/prod/kustomization.yaml b/infra/redis/overlays/prod/kustomization.yaml deleted file mode 100644 index f928b81..0000000 --- a/infra/redis/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -patches: - - target: - kind: Deployment - name: redis - patch: |- - - op: replace - path: /spec/replicas - value: 3 - - op: replace - path: /spec/template/spec/containers/0/resources/limits/memory - value: "2Gi" diff --git a/infra/redis/overlays/stage/kustomization.yaml b/infra/redis/overlays/stage/kustomization.yaml deleted file mode 100644 index 0ba21da..0000000 --- a/infra/redis/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage- diff --git a/infra/s3/base/deployment.yaml b/infra/s3/base/deployment.yaml deleted file mode 100644 index bf9c896..0000000 --- a/infra/s3/base/deployment.yaml +++ /dev/null @@ -1,60 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: minio - namespace: data-layer -spec: - replicas: 1 - selector: - matchLabels: - app: minio - template: - metadata: - labels: - app: minio - spec: - containers: - - name: minio - image: minio/minio:RELEASE.2023-08-09T23-30-22Z - args: - - server - - /data - - --console-address - - ":9001" - env: - - name: MINIO_ROOT_USER - value: "minioadmin" - - name: MINIO_ROOT_PASSWORD - value: "minioadmin" - ports: - - containerPort: 9000 - - containerPort: 9001 - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "256Mi" - cpu: "500m" - volumeMounts: - - name: minio-data - mountPath: /data - volumes: - - name: minio-data - emptyDir: {} ---- -apiVersion: v1 -kind: Service -metadata: - name: minio - namespace: data-layer -spec: - ports: - - name: api - port: 9000 - targetPort: 9000 - - name: console - port: 9001 - targetPort: 9001 - selector: - app: minio diff --git a/infra/s3/base/kustomization.yaml b/infra/s3/base/kustomization.yaml deleted file mode 100644 index 9c2d28b..0000000 --- a/infra/s3/base/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - deployment.yaml diff --git a/infra/s3/overlays/dev/kustomization.yaml b/infra/s3/overlays/dev/kustomization.yaml deleted file mode 100644 index 88dffbe..0000000 --- a/infra/s3/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: dev- diff --git a/infra/s3/overlays/prod/kustomization.yaml b/infra/s3/overlays/prod/kustomization.yaml deleted file mode 100644 index 2333422..0000000 --- a/infra/s3/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base diff --git a/infra/s3/overlays/stage/kustomization.yaml b/infra/s3/overlays/stage/kustomization.yaml deleted file mode 100644 index 0ba21da..0000000 --- a/infra/s3/overlays/stage/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -namePrefix: stage-