From 0e95d99868087d36187c4f6efb4a2eb23676b0f3 Mon Sep 17 00:00:00 2001 From: ypso Date: Tue, 21 Jul 2026 00:35:57 +0200 Subject: [PATCH 1/8] feat(auth): add generic OIDC backend sessions --- .env.example | 22 ++- INSTALLATION.md | 28 ++- backend/app/api/deps.py | 55 ++++-- backend/app/api/routes/auth.py | 98 ++++++++++- backend/app/api/routes/status.py | 40 +++-- backend/app/core/config.py | 40 ++++- backend/app/core/security.py | 184 +++++++++++++++++++- backend/app/main.py | 13 +- backend/requirements.txt | 4 +- backend/tests/test_auth.py | 281 ++++++++++++++++++++++++++++++- backend/tests/test_settings.py | 45 +++++ 11 files changed, 772 insertions(+), 38 deletions(-) diff --git a/.env.example b/.env.example index 0ddb255..0eb0a5f 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ # Backend - server-side only (NEVER commit .env) +# Generate: python3 -c "import secrets; print(secrets.token_hex(32))" SECRET_KEY=change_me_in_production SQLITE_PATH=./data/homelab.db # Uploaded media (floor plans) folder. Optional — defaults to /uploads, @@ -7,13 +8,30 @@ SQLITE_PATH=./data/homelab.db # Set this to the URL(s) you use to access Homelable in your browser. CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"] -# Auth — default credentials: admin / admin +# Auth — local mode is the backward-compatible default. +AUTH_MODE=local +# Local credentials: admin / admin by default. # ⚠️ Change before exposing on a network. -# Generate a new hash: python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('yourpassword'))" +# Generate a new hash: python3 -c "import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())" # ⚠️ Keep the single quotes around the hash — bcrypt hashes contain \$ which Docker misinterprets without them. AUTH_USERNAME=admin AUTH_PASSWORD_HASH='$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG' +# OpenID Connect (optional alternative to local auth). +# Set AUTH_MODE=oidc and configure every value below. The callback must be +# registered exactly at the identity provider. Keep CORS_ORIGINS restricted to +# the Homelable browser origin; wildcard CORS is rejected in OIDC mode. OIDC +# also requires SECRET_KEY to contain at least 32 bytes. +# AUTH_MODE=oidc +# OIDC_DISCOVERY_URL=https://idp.example/application/o/homelable/.well-known/openid-configuration +# OIDC_CLIENT_ID=homelable +# OIDC_CLIENT_SECRET=replace-with-a-secret +# OIDC_REDIRECT_URI=https://homelable.example/api/v1/auth/oidc/callback +# OIDC_SCOPES="openid profile email" +# OIDC_COOKIE_SECURE=true +# OIDC_SESSION_EXPIRE_MINUTES=480 +# OIDC_TRANSACTION_EXPIRE_SECONDS=600 + # Scanner — JSON array of CIDR ranges to scan SCANNER_RANGES=["192.168.1.0/24"] diff --git a/INSTALLATION.md b/INSTALLATION.md index 7837f96..739eb7a 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -13,7 +13,7 @@ Open **http://localhost:3000** — login with `admin` / `admin`. > Generate a new hash: ```bash -docker compose exec backend python -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('yourpassword'))" +docker compose exec backend python -c 'import bcrypt; print(bcrypt.hashpw(b"yourpassword", bcrypt.gensalt()).decode())' ``` @@ -21,7 +21,7 @@ docker compose exec backend python -c "from passlib.context import CryptContext; - **`.env` file** (recommended): wrap the hash in single quotes → `AUTH_PASSWORD_HASH='$2b$12$...'` - **`docker-compose.yml` `environment:` block**: escape every `$` as `$$` — use this command to generate a pre-escaped hash: ```bash - docker compose exec backend python -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('yourpassword').replace('\$', '\$\$'))" + docker compose exec backend python -c 'import bcrypt; print(bcrypt.hashpw(b"yourpassword", bcrypt.gensalt()).decode().replace("$", "$$"))' ``` ## Quick Start — Frontend only @@ -69,6 +69,7 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Proxmo All configuration is done via `.env` (copied from `.env.example`): ```env +# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" SECRET_KEY=change_me_in_production # Auth — default: admin / admin @@ -82,6 +83,29 @@ SCANNER_RANGES=["192.168.1.0/24"] STATUS_CHECKER_INTERVAL=60 ``` +### OpenID Connect (optional) + +OIDC is an exclusive alternative to the local password. Existing installs stay +in `AUTH_MODE=local` unless explicitly changed. + +```env +AUTH_MODE=oidc +CORS_ORIGINS=["https://homelable.example"] +OIDC_DISCOVERY_URL=https://idp.example/application/o/homelable/.well-known/openid-configuration +OIDC_CLIENT_ID=homelable +OIDC_CLIENT_SECRET=replace-with-a-secret +OIDC_REDIRECT_URI=https://homelable.example/api/v1/auth/oidc/callback +OIDC_SCOPES="openid profile email" +OIDC_COOKIE_SECURE=true +OIDC_SESSION_EXPIRE_MINUTES=480 +``` + +Register `OIDC_REDIRECT_URI` exactly at the identity provider. Production OIDC +requires HTTPS, a non-wildcard `CORS_ORIGINS`, and a confidential client. The +backend uses Authorization Code with PKCE and keeps provider tokens out of the +browser. `SECRET_KEY` must contain at least 32 bytes in OIDC mode. Do not expose +`OIDC_CLIENT_SECRET` in Compose YAML or commit it to Git. + All settings are also editable in-app via the **Scan Network** button. --- diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 260c5d4..d09d757 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -1,31 +1,66 @@ import hmac +from dataclasses import dataclass from fastapi import Depends, Header, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from app.core.config import settings -from app.core.security import decode_token +from app.core.security import decode_token, get_oidc_session bearer = HTTPBearer(auto_error=False) -def get_current_user( +@dataclass(frozen=True) +class AuthContext: + subject: str + display_name: str + auth_method: str + issuer: str | None = None + csrf_token: str | None = None + + +def get_auth_context( request: Request, credentials: HTTPAuthorizationCredentials | None = Depends(bearer), x_mcp_service_key: str | None = Header(default=None), -) -> str: +) -> AuthContext: # 1. MCP service key (Docker-internal only — backend port is not externally exposed) if x_mcp_service_key is not None: if not settings.mcp_service_key: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="MCP service key not configured") if not hmac.compare_digest(x_mcp_service_key.encode(), settings.mcp_service_key.encode()): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid MCP service key") - return "__mcp_service__" + return AuthContext( + subject="__mcp_service__", + display_name="MCP service", + auth_method="mcp", + ) # 2. Standard JWT bearer token - if credentials is None: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") - username = decode_token(credentials.credentials) - if not username: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") - return username + if credentials is not None: + username = decode_token(credentials.credentials) + if not username: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + return AuthContext( + subject=username, + display_name=username, + auth_method="local", + ) + + # 3. OIDC session cookie (only active in exclusive OIDC mode) + if settings.auth_mode == "oidc": + oidc_session = get_oidc_session(request) + if oidc_session is not None: + return AuthContext( + subject=oidc_session.subject, + display_name=oidc_session.display_name, + auth_method="oidc", + issuer=oidc_session.issuer, + csrf_token=oidc_session.csrf_token, + ) + + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") + + +def get_current_user(context: AuthContext = Depends(get_auth_context)) -> str: + return context.subject diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index adead57..2468c37 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -1,12 +1,26 @@ import hmac +import logging +from collections.abc import Mapping +from typing import cast -from fastapi import APIRouter, HTTPException, status +from authlib.integrations.base_client.errors import OAuthError +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from pydantic import BaseModel +from starlette.responses import RedirectResponse +from app.api.deps import AuthContext, get_auth_context from app.core.config import settings -from app.core.security import create_access_token, verify_password +from app.core.security import ( + clear_oidc_session_cookie, + create_access_token, + create_oidc_session_token, + get_oidc_client, + set_oidc_session_cookie, + verify_password, +) router = APIRouter() +logger = logging.getLogger(__name__) class LoginRequest(BaseModel): @@ -19,8 +33,36 @@ class TokenResponse(BaseModel): token_type: str = "bearer" +class AuthConfigResponse(BaseModel): + mode: str + oidc_login_url: str | None = None + + +class CurrentUserResponse(BaseModel): + subject: str + display_name: str + auth_method: str + issuer: str | None = None + csrf_token: str | None = None + + +def _require_oidc_mode() -> None: + if settings.auth_mode != "oidc": + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="OIDC login is disabled") + + +@router.get("/config", response_model=AuthConfigResponse) +async def auth_config() -> AuthConfigResponse: + return AuthConfigResponse( + mode=settings.auth_mode, + oidc_login_url="/api/v1/auth/oidc/login" if settings.auth_mode == "oidc" else None, + ) + + @router.post("/login", response_model=TokenResponse) async def login(body: LoginRequest) -> TokenResponse: + if settings.auth_mode != "local": + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Local login is disabled") # Always run both checks to prevent timing-based username enumeration. # hmac.compare_digest is constant-time; verify_password (bcrypt) always runs. username_ok = hmac.compare_digest(body.username, settings.auth_username) @@ -29,3 +71,55 @@ async def login(body: LoginRequest) -> TokenResponse: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") token = create_access_token(body.username) return TokenResponse(access_token=token) + + +@router.get("/oidc/login") +async def oidc_login(request: Request) -> Response: + _require_oidc_mode() + client = get_oidc_client() + return cast(Response, await client.authorize_redirect(request, settings.oidc_redirect_uri)) + + +@router.get("/oidc/callback") +async def oidc_callback(request: Request) -> Response: + _require_oidc_mode() + client = get_oidc_client() + try: + token = await client.authorize_access_token(request) + except OAuthError as exc: + logger.warning("OIDC callback rejected: %s", exc.error) + request.session.clear() + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="OIDC authentication failed") from None + + userinfo = token.get("userinfo") + if not isinstance(userinfo, Mapping): + request.session.clear() + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="OIDC authentication failed") + try: + session_token = create_oidc_session_token(userinfo) + except ValueError: + request.session.clear() + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="OIDC authentication failed") from None + + request.session.clear() + response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER) + set_oidc_session_cookie(response, session_token) + return response + + +@router.get("/me", response_model=CurrentUserResponse) +async def current_user(context: AuthContext = Depends(get_auth_context)) -> CurrentUserResponse: + return CurrentUserResponse( + subject=context.subject, + display_name=context.display_name, + auth_method=context.auth_method, + issuer=context.issuer, + csrf_token=context.csrf_token, + ) + + +@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) +async def logout(_: AuthContext = Depends(get_auth_context)) -> Response: + response = Response(status_code=status.HTTP_204_NO_CONTENT) + clear_oidc_session_cookie(response) + return response diff --git a/backend/app/api/routes/status.py b/backend/app/api/routes/status.py index 26c5c4f..27db337 100644 --- a/backend/app/api/routes/status.py +++ b/backend/app/api/routes/status.py @@ -3,7 +3,13 @@ import json from fastapi import APIRouter, WebSocket, WebSocketDisconnect -from app.core.security import decode_token +from app.core.config import settings +from app.core.security import ( + decode_oidc_session_token, + decode_token, + oidc_session_cookie_name, + origin_is_allowed, +) router = APIRouter() @@ -21,19 +27,29 @@ def _drop(websocket: WebSocket) -> None: async def ws_status(websocket: WebSocket) -> None: # Accept first so we can send a close frame with a reason code await websocket.accept() - try: - # Expect the first message to be a JSON auth payload: {"token": ""} - raw = await websocket.receive_text() - try: - payload = json.loads(raw) - token = payload.get("token", "") - except (json.JSONDecodeError, AttributeError): - token = "" - if not token or not decode_token(token): + if not origin_is_allowed(websocket.headers.get("origin")): + await websocket.close(code=1008) # Policy Violation + return + + if settings.auth_mode == "oidc": + cookie_token = websocket.cookies.get(oidc_session_cookie_name(), "") + if not cookie_token or decode_oidc_session_token(cookie_token) is None: await websocket.close(code=1008) # Policy Violation return - except WebSocketDisconnect: - return + else: + # Local mode keeps the existing first-message bearer protocol. + try: + raw = await websocket.receive_text() + try: + payload = json.loads(raw) + token = payload.get("token", "") + except (json.JSONDecodeError, AttributeError): + token = "" + if not token or not decode_token(token): + await websocket.close(code=1008) # Policy Violation + return + except WebSocketDisconnect: + return _connections.append(websocket) try: diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 7c24d01..ec47043 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,8 +1,9 @@ import json import logging from pathlib import Path +from typing import Literal -from pydantic import model_validator +from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict logger = logging.getLogger(__name__) @@ -35,11 +36,22 @@ class Settings(BaseSettings): access_token_expire_minutes: int = 1440 # 24h # Auth — set AUTH_USERNAME and AUTH_PASSWORD_HASH in .env + auth_mode: Literal["local", "oidc"] = "local" auth_username: str = "admin" auth_password_hash: str = "" + # OpenID Connect — required only when AUTH_MODE=oidc. + oidc_discovery_url: str = "" + oidc_client_id: str = "" + oidc_client_secret: str = "" + oidc_redirect_uri: str = "" + oidc_scopes: str = "openid profile email" + oidc_cookie_secure: bool = True + oidc_session_expire_minutes: int = Field(default=480, ge=5, le=1440) + oidc_transaction_expire_seconds: int = Field(default=600, ge=60, le=3600) + @model_validator(mode="after") - def check_password_hash(self) -> "Settings": + def validate_auth_settings(self) -> "Settings": h = self.auth_password_hash if h and not h.startswith("$2"): logger.error( @@ -47,6 +59,30 @@ class Settings(BaseSettings): "bcrypt hashes contain '$' signs — wrap the value in single quotes " "in your .env file: AUTH_PASSWORD_HASH='$2b$12$...'" ) + if self.auth_mode == "oidc": + required = { + "OIDC_DISCOVERY_URL": self.oidc_discovery_url, + "OIDC_CLIENT_ID": self.oidc_client_id, + "OIDC_CLIENT_SECRET": self.oidc_client_secret, + "OIDC_REDIRECT_URI": self.oidc_redirect_uri, + } + missing = [name for name, value in required.items() if not value] + if missing: + raise ValueError(f"AUTH_MODE=oidc requires: {', '.join(missing)}") + if len(self.secret_key.encode("utf-8")) < 32: + raise ValueError("SECRET_KEY must be at least 32 bytes when AUTH_MODE=oidc") + if "openid" not in self.oidc_scopes.split(): + raise ValueError("OIDC_SCOPES must contain 'openid'") + for name, value in { + "OIDC_DISCOVERY_URL": self.oidc_discovery_url, + "OIDC_REDIRECT_URI": self.oidc_redirect_uri, + }.items(): + if not value.startswith(("https://", "http://")): + raise ValueError(f"{name} must be an HTTP(S) URL") + if self.oidc_cookie_secure and not self.oidc_redirect_uri.startswith("https://"): + raise ValueError("OIDC_REDIRECT_URI must use HTTPS when OIDC_COOKIE_SECURE=true") + if "*" in self.cors_origins: + raise ValueError("CORS_ORIGINS cannot contain '*' when AUTH_MODE=oidc") return self # Scanner diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 957b1aa..d893ad9 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -1,10 +1,33 @@ +import hmac +import secrets +from collections.abc import Mapping +from dataclasses import dataclass from datetime import datetime, timedelta, timezone +from functools import lru_cache +from typing import Any import bcrypt -from jose import JWTError, jwt +import jwt +from authlib.integrations.starlette_client import OAuth, StarletteOAuth2App +from fastapi import Request, Response, status +from jwt import InvalidTokenError +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import JSONResponse from app.core.config import settings +ACCESS_TOKEN_USE = "access" +OIDC_SESSION_TOKEN_USE = "oidc_session" +SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "TRACE"}) + + +@dataclass(frozen=True) +class OIDCSession: + subject: str + issuer: str + display_name: str + csrf_token: str + def verify_password(plain: str, hashed: str) -> bool: if not plain or not hashed: @@ -21,14 +44,169 @@ def hash_password(password: str) -> str: def create_access_token(subject: str) -> str: expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) - payload = {"sub": subject, "exp": expire} + payload = {"sub": subject, "exp": expire, "token_use": ACCESS_TOKEN_USE} return str(jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)) def decode_token(token: str) -> str | None: try: payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) + if payload.get("token_use") not in (None, ACCESS_TOKEN_USE): + return None sub = payload.get("sub") return str(sub) if sub is not None else None - except JWTError: + except InvalidTokenError: return None + + +def create_oidc_session_token(userinfo: Mapping[str, Any]) -> str: + subject = userinfo.get("sub") + issuer = userinfo.get("iss") + if not isinstance(subject, str) or not subject or not isinstance(issuer, str) or not issuer: + raise ValueError("OIDC userinfo must contain non-empty 'sub' and 'iss' claims") + + display_name = next( + ( + value + for value in ( + userinfo.get("preferred_username"), + userinfo.get("name"), + userinfo.get("email"), + subject, + ) + if isinstance(value, str) and value + ), + subject, + ) + now = datetime.now(timezone.utc) + payload = { + "sub": subject, + "oidc_issuer": issuer, + "name": display_name, + "csrf": secrets.token_urlsafe(32), + "iat": now, + "exp": now + timedelta(minutes=settings.oidc_session_expire_minutes), + "token_use": OIDC_SESSION_TOKEN_USE, + } + return str(jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)) + + +def decode_oidc_session_token(token: str) -> OIDCSession | None: + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) + if payload.get("token_use") != OIDC_SESSION_TOKEN_USE: + return None + subject = payload.get("sub") + issuer = payload.get("oidc_issuer") + display_name = payload.get("name") + csrf_token = payload.get("csrf") + if not isinstance(subject, str) or not subject: + return None + if not isinstance(issuer, str) or not issuer: + return None + if not isinstance(display_name, str) or not display_name: + return None + if not isinstance(csrf_token, str) or not csrf_token: + return None + return OIDCSession( + subject=subject, + issuer=issuer, + display_name=display_name, + csrf_token=csrf_token, + ) + except InvalidTokenError: + return None + + +def oidc_session_cookie_name() -> str: + return "__Host-homelable-session" if settings.oidc_cookie_secure else "homelable-session" + + +def set_oidc_session_cookie(response: Response, token: str) -> None: + response.set_cookie( + key=oidc_session_cookie_name(), + value=token, + max_age=settings.oidc_session_expire_minutes * 60, + secure=settings.oidc_cookie_secure, + httponly=True, + samesite="lax", + path="/", + ) + + +def clear_oidc_session_cookie(response: Response) -> None: + response.delete_cookie( + key=oidc_session_cookie_name(), + secure=settings.oidc_cookie_secure, + httponly=True, + samesite="lax", + path="/", + ) + + +def get_oidc_session(request: Request) -> OIDCSession | None: + token = request.cookies.get(oidc_session_cookie_name()) + return decode_oidc_session_token(token) if token else None + + +def origin_is_allowed(origin: str | None) -> bool: + if not origin: + return False + allowed_origins = {value.rstrip("/") for value in settings.cors_origins} + return origin.rstrip("/") in allowed_origins + + +class OIDCCSRFMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + if settings.auth_mode != "oidc" or request.method in SAFE_METHODS: + return await call_next(request) + + # Explicit non-cookie credentials take precedence in get_current_user, + # so their requests keep the existing CSRF-free API behavior. + if request.headers.get("Authorization") or request.headers.get("X-MCP-Service-Key"): + return await call_next(request) + + oidc_session = get_oidc_session(request) + if oidc_session is None: + return await call_next(request) + + csrf_token = request.headers.get("X-Homelable-CSRF", "") + csrf_valid = hmac.compare_digest(csrf_token.encode(), oidc_session.csrf_token.encode()) + if not csrf_valid or not origin_is_allowed(request.headers.get("Origin")): + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content={"detail": "CSRF validation failed"}, + ) + return await call_next(request) + + +@lru_cache(maxsize=8) +def _build_oidc_client( + discovery_url: str, + client_id: str, + client_secret: str, + scopes: str, +) -> StarletteOAuth2App: + oauth = OAuth() + client = oauth.register( + name="oidc", + client_id=client_id, + client_secret=client_secret, + server_metadata_url=discovery_url, + client_kwargs={ + "scope": scopes, + "code_challenge_method": "S256", + }, + ) + if client is None: + raise RuntimeError("Failed to register OIDC client") + return client + + +def get_oidc_client() -> StarletteOAuth2App: + return _build_oidc_client( + settings.oidc_discovery_url, + settings.oidc_client_id, + settings.oidc_client_secret, + settings.oidc_scopes, + ) diff --git a/backend/app/main.py b/backend/app/main.py index 368a490..709231e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6,6 +6,7 @@ from typing import Any from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware from app.api.routes import ( auth, @@ -25,6 +26,7 @@ from app.api.routes import ( from app.api.routes import settings as settings_routes from app.core.config import settings from app.core.scheduler import start_scheduler, stop_scheduler +from app.core.security import OIDCCSRFMiddleware from app.db.database import init_db @@ -53,12 +55,21 @@ app = FastAPI( lifespan=lifespan, ) +app.add_middleware(OIDCCSRFMiddleware) +app.add_middleware( + SessionMiddleware, + secret_key=settings.secret_key, + session_cookie=("__Host-homelable-oidc-flow" if settings.oidc_cookie_secure else "homelable-oidc-flow"), + max_age=settings.oidc_transaction_expire_seconds, + same_site="lax", + https_only=settings.oidc_cookie_secure, +) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], - allow_headers=["Authorization", "Content-Type"], + allow_headers=["Authorization", "Content-Type", "X-Homelable-CSRF", "X-MCP-Service-Key"], ) app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) diff --git a/backend/requirements.txt b/backend/requirements.txt index 0b02927..7837f7b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,8 +6,10 @@ aiosqlite==0.20.0 alembic==1.13.3 pydantic==2.9.2 pydantic-settings==2.5.2 -python-jose[cryptography]==3.5.0 +PyJWT==2.13.0 bcrypt==4.2.1 +authlib==1.7.2 +itsdangerous==2.2.0 python-multipart==0.0.31 apscheduler==3.10.4 python-nmap==0.7.1 diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 09b569d..76823b7 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1,5 +1,55 @@ +import time +from unittest.mock import AsyncMock, patch +from urllib.parse import parse_qs, urlparse + import pytest +from authlib.integrations.base_client.errors import OAuthError +from fastapi import WebSocketDisconnect from httpx import AsyncClient +from starlette.responses import RedirectResponse +from starlette.testclient import TestClient + + +class FakeOIDCClient: + def __init__(self, token=None, error: OAuthError | None = None): + self.token = token or {} + self.error = error + self.redirect_uri: str | None = None + + async def authorize_redirect(self, _request, redirect_uri: str): + self.redirect_uri = redirect_uri + return RedirectResponse("https://idp.example/application/o/authorize/?state=test", status_code=302) + + async def authorize_access_token(self, _request): + if self.error is not None: + raise self.error + return self.token + + +@pytest.fixture +def oidc_settings(): + from app.core.config import settings + from app.core.security import _build_oidc_client + + values = { + "auth_mode": "oidc", + "oidc_discovery_url": "https://idp.example/application/o/homelable/.well-known/openid-configuration", + "oidc_client_id": "homelable", + "oidc_client_secret": "test-client-secret", + "oidc_redirect_uri": "http://test/api/v1/auth/oidc/callback", + "oidc_scopes": "openid profile email", + "oidc_cookie_secure": False, + "oidc_session_expire_minutes": 60, + "cors_origins": ["http://test"], + } + original = {name: getattr(settings, name) for name in values} + for name, value in values.items(): + setattr(settings, name, value) + _build_oidc_client.cache_clear() + yield settings + for name, value in original.items(): + setattr(settings, name, value) + _build_oidc_client.cache_clear() async def test_login_success(client: AsyncClient): @@ -10,6 +60,12 @@ async def test_login_success(client: AsyncClient): assert data["token_type"] == "bearer" +async def test_auth_config_defaults_to_local(client: AsyncClient): + res = await client.get("/api/v1/auth/config") + assert res.status_code == 200 + assert res.json() == {"mode": "local", "oidc_login_url": None} + + async def test_login_wrong_password(client: AsyncClient): res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "wrong"}) assert res.status_code == 401 @@ -76,7 +132,7 @@ async def test_expired_token_rejected(client: AsyncClient): """A JWT whose `exp` is in the past must be refused.""" from datetime import datetime, timedelta, timezone - from jose import jwt + import jwt from app.core.config import settings payload = { @@ -97,14 +153,14 @@ async def test_token_signed_with_wrong_secret_rejected(client: AsyncClient): """A token signed with a different key must not be accepted.""" from datetime import datetime, timedelta, timezone - from jose import jwt + import jwt from app.core.config import settings payload = { "sub": "admin", "exp": datetime.now(timezone.utc) + timedelta(minutes=5), } - forged = jwt.encode(payload, "different-secret", algorithm=settings.algorithm) + forged = jwt.encode(payload, "different-secret-that-is-at-least-32-bytes", algorithm=settings.algorithm) res = await client.get("/api/v1/nodes", headers={"Authorization": f"Bearer {forged}"}) assert res.status_code == 401 @@ -136,3 +192,222 @@ def test_verify_password_handles_empty_inputs(): assert verify_password("", h) is False assert verify_password("hunter2", "") is False assert verify_password("", "") is False + + +# --- OpenID Connect --- + +async def test_oidc_config_exposes_login_url(client: AsyncClient, oidc_settings): + res = await client.get("/api/v1/auth/config") + assert res.status_code == 200 + assert res.json() == { + "mode": "oidc", + "oidc_login_url": "/api/v1/auth/oidc/login", + } + + +async def test_local_login_is_disabled_in_oidc_mode(client: AsyncClient, oidc_settings): + res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) + assert res.status_code == 404 + + +async def test_oidc_login_is_hidden_in_local_mode(client: AsyncClient): + res = await client.get("/api/v1/auth/oidc/login") + assert res.status_code == 404 + + +async def test_oidc_login_uses_configured_callback(client: AsyncClient, oidc_settings): + fake_client = FakeOIDCClient() + with patch("app.api.routes.auth.get_oidc_client", return_value=fake_client): + res = await client.get("/api/v1/auth/oidc/login") + assert res.status_code == 302 + assert res.headers["location"].startswith("https://idp.example/") + assert fake_client.redirect_uri == "http://test/api/v1/auth/oidc/callback" + + +async def test_oidc_client_enables_pkce_s256_and_nonce(oidc_settings): + from app.core.security import get_oidc_client + + oidc_client = get_oidc_client() + metadata = {"authorization_endpoint": "https://idp.example/application/o/authorize/"} + with patch.object(oidc_client, "load_server_metadata", AsyncMock(return_value=metadata)): + authorization = await oidc_client.create_authorization_url(oidc_settings.oidc_redirect_uri) + + query = parse_qs(urlparse(authorization["url"]).query) + assert query["response_type"] == ["code"] + assert query["code_challenge_method"] == ["S256"] + assert "code_challenge" in query + assert "code_verifier" not in query + assert query["nonce"] == [authorization["nonce"]] + assert authorization["code_verifier"] + + +async def test_oidc_callback_creates_session_for_api(client: AsyncClient, oidc_settings): + fake_client = FakeOIDCClient(token={ + "userinfo": { + "iss": "https://idp.example/application/o/homelable/", + "sub": "user-123", + "preferred_username": "alice", + }, + }) + with patch("app.api.routes.auth.get_oidc_client", return_value=fake_client): + callback = await client.get("/api/v1/auth/oidc/callback") + + assert callback.status_code == 303 + assert callback.headers["location"] == "/" + cookie = callback.headers["set-cookie"] + assert "homelable-session=" in cookie + assert "HttpOnly" in cookie + assert "SameSite=lax" in cookie + + me = await client.get("/api/v1/auth/me") + assert me.status_code == 200 + assert me.json()["subject"] == "user-123" + assert me.json()["display_name"] == "alice" + assert me.json()["auth_method"] == "oidc" + assert me.json()["issuer"] == "https://idp.example/application/o/homelable/" + assert me.json()["csrf_token"] + + protected = await client.get("/api/v1/nodes") + assert protected.status_code == 200 + + +@pytest.mark.parametrize("userinfo", [None, {}, {"sub": "user-123"}, {"iss": "https://idp.example/"}]) +async def test_oidc_callback_rejects_missing_identity_claims(client: AsyncClient, oidc_settings, userinfo): + token = {} if userinfo is None else {"userinfo": userinfo} + fake_client = FakeOIDCClient(token=token) + with patch("app.api.routes.auth.get_oidc_client", return_value=fake_client): + res = await client.get("/api/v1/auth/oidc/callback") + assert res.status_code == 401 + assert res.json() == {"detail": "OIDC authentication failed"} + + +async def test_oidc_callback_hides_protocol_error_details(client: AsyncClient, oidc_settings): + fake_client = FakeOIDCClient(error=OAuthError(error="invalid_grant", description="sensitive detail")) + with patch("app.api.routes.auth.get_oidc_client", return_value=fake_client): + res = await client.get("/api/v1/auth/oidc/callback") + assert res.status_code == 401 + assert res.json() == {"detail": "OIDC authentication failed"} + assert "sensitive detail" not in res.text + + +async def test_oidc_session_token_cannot_be_used_as_bearer(client: AsyncClient, oidc_settings): + from app.core.security import create_oidc_session_token + + token = create_oidc_session_token({"iss": "https://idp.example/", "sub": "user-123"}) + res = await client.get("/api/v1/nodes", headers={"Authorization": f"Bearer {token}"}) + assert res.status_code == 401 + + +async def test_oidc_cookie_mutations_require_csrf_and_allowed_origin(client: AsyncClient, oidc_settings): + fake_client = FakeOIDCClient(token={ + "userinfo": { + "iss": "https://idp.example/application/o/homelable/", + "sub": "user-123", + "preferred_username": "alice", + }, + }) + with patch("app.api.routes.auth.get_oidc_client", return_value=fake_client): + await client.get("/api/v1/auth/oidc/callback") + csrf_token = (await client.get("/api/v1/auth/me")).json()["csrf_token"] + + missing = await client.post("/api/v1/auth/logout") + assert missing.status_code == 403 + wrong_origin = await client.post( + "/api/v1/auth/logout", + headers={"Origin": "https://evil.example", "X-Homelable-CSRF": csrf_token}, + ) + assert wrong_origin.status_code == 403 + valid = await client.post( + "/api/v1/auth/logout", + headers={"Origin": "http://test", "X-Homelable-CSRF": csrf_token}, + ) + assert valid.status_code == 204 + assert "Max-Age=0" in valid.headers["set-cookie"] + assert (await client.get("/api/v1/auth/me")).status_code == 401 + + +async def test_local_bearer_logout_does_not_require_csrf(client: AsyncClient, headers): + res = await client.post("/api/v1/auth/logout", headers=headers) + assert res.status_code == 204 + + +# --- WebSocket authentication --- + +def _wait_for_connection_count(expected: int) -> None: + from app.api.routes import status as status_routes + + deadline = time.monotonic() + 1 + while len(status_routes._connections) != expected and time.monotonic() < deadline: + time.sleep(0.01) + assert len(status_routes._connections) == expected + + +def test_local_websocket_keeps_first_message_bearer_flow(): + from app.core.security import create_access_token + from app.main import app + + test_client = TestClient(app) + try: + with test_client.websocket_connect( + "/api/v1/status/ws/status", + headers={"Origin": "http://localhost:3000"}, + ) as websocket: + websocket.send_json({"token": create_access_token("admin")}) + _wait_for_connection_count(1) + _wait_for_connection_count(0) + finally: + test_client.close() + + +def test_websocket_rejects_untrusted_origin_before_auth(): + from app.main import app + + test_client = TestClient(app) + try: + with ( + pytest.raises(WebSocketDisconnect) as caught, + test_client.websocket_connect( + "/api/v1/status/ws/status", + headers={"Origin": "https://evil.example"}, + ) as websocket, + ): + websocket.receive_text() + assert caught.value.code == 1008 + finally: + test_client.close() + + +def test_oidc_websocket_uses_session_cookie_without_javascript_token(oidc_settings): + from app.core.security import create_oidc_session_token, oidc_session_cookie_name + from app.main import app + + session_token = create_oidc_session_token({"iss": "https://idp.example/", "sub": "user-123"}) + test_client = TestClient(app) + test_client.cookies.set(oidc_session_cookie_name(), session_token) + try: + with test_client.websocket_connect( + "/api/v1/status/ws/status", + headers={"Origin": "http://test"}, + ): + _wait_for_connection_count(1) + _wait_for_connection_count(0) + finally: + test_client.close() + + +def test_oidc_websocket_rejects_missing_session_cookie(oidc_settings): + from app.main import app + + test_client = TestClient(app) + try: + with ( + pytest.raises(WebSocketDisconnect) as caught, + test_client.websocket_connect( + "/api/v1/status/ws/status", + headers={"Origin": "http://test"}, + ) as websocket, + ): + websocket.receive_text() + assert caught.value.code == 1008 + finally: + test_client.close() diff --git a/backend/tests/test_settings.py b/backend/tests/test_settings.py index 963b321..de68e07 100644 --- a/backend/tests/test_settings.py +++ b/backend/tests/test_settings.py @@ -167,3 +167,48 @@ def test_save_overrides_omits_mesh_credentials(tmp_path): assert written["zigbee_sync_enabled"] is True assert written["zigbee_sync_interval"] == 900 assert written["zwave_sync_interval"] == 1200 + + +def _valid_oidc_settings(**overrides): + values = { + "secret_key": "test-secret-key-that-is-at-least-32-bytes", + "auth_mode": "oidc", + "oidc_discovery_url": "https://idp.example/application/o/homelable/.well-known/openid-configuration", + "oidc_client_id": "homelable", + "oidc_client_secret": "client-secret", + "oidc_redirect_uri": "https://homelable.example/api/v1/auth/oidc/callback", + "cors_origins": ["https://homelable.example"], + } + values.update(overrides) + return Settings(_env_file=None, **values) + + +def test_oidc_mode_requires_complete_client_configuration(): + with pytest.raises(ValueError, match="OIDC_DISCOVERY_URL.*OIDC_CLIENT_ID.*OIDC_CLIENT_SECRET.*OIDC_REDIRECT_URI"): + Settings(secret_key="test-secret", auth_mode="oidc", _env_file=None) + + +def test_oidc_mode_requires_openid_scope(): + with pytest.raises(ValueError, match="OIDC_SCOPES must contain 'openid'"): + _valid_oidc_settings(oidc_scopes="profile email") + + +def test_oidc_mode_requires_strong_session_secret(): + with pytest.raises(ValueError, match="SECRET_KEY must be at least 32 bytes"): + _valid_oidc_settings(secret_key="too-short") + + +def test_oidc_secure_cookie_requires_https_callback(): + with pytest.raises(ValueError, match="must use HTTPS"): + _valid_oidc_settings(oidc_redirect_uri="http://localhost/api/v1/auth/oidc/callback") + + +def test_oidc_mode_rejects_wildcard_cors(): + with pytest.raises(ValueError, match="CORS_ORIGINS cannot contain"): + _valid_oidc_settings(cors_origins=["*"]) + + +def test_valid_oidc_configuration_is_accepted(): + configured = _valid_oidc_settings() + assert configured.auth_mode == "oidc" + assert configured.oidc_cookie_secure is True From fd96b7d6ae83491eaf5fc6e680de180707650679 Mon Sep 17 00:00:00 2001 From: ypso Date: Tue, 21 Jul 2026 00:36:02 +0200 Subject: [PATCH 2/8] feat(auth): add OIDC browser session flow --- frontend/src/App.tsx | 17 ++- frontend/src/api/__tests__/client.test.ts | 30 ++++- frontend/src/api/client.ts | 11 +- frontend/src/auth/__tests__/bootstrap.test.ts | 121 +++++++++++++++++ frontend/src/auth/bootstrap.ts | 33 +++++ frontend/src/components/LoginPage.tsx | 122 +++++++++++------- .../components/__tests__/LoginPage.test.tsx | 37 +++++- frontend/src/components/panels/Sidebar.tsx | 13 +- .../panels/__tests__/Sidebar.test.tsx | 13 +- .../hooks/__tests__/useStatusPolling.test.ts | 18 +++ frontend/src/hooks/useStatusPolling.ts | 16 ++- .../src/stores/__tests__/authStore.test.ts | 46 ++++++- frontend/src/stores/authStore.ts | 66 +++++++++- 13 files changed, 474 insertions(+), 69 deletions(-) create mode 100644 frontend/src/auth/__tests__/bootstrap.test.ts create mode 100644 frontend/src/auth/bootstrap.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c0fda1c..7dcac07 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -51,6 +51,7 @@ import { WalkthroughInvite } from '@/walkthrough/WalkthroughInvite' import { WalkthroughOverlay } from '@/walkthrough/WalkthroughOverlay' import { DEMO_SCAN_RUNS, DEMO_PENDING_DEVICES } from '@/walkthrough/demoTourData' import { useStatusPolling } from '@/hooks/useStatusPolling' +import { bootstrapAuth } from '@/auth/bootstrap' import type { NodeData, EdgeData, CustomStyleDef, FloorMapConfig, NodeType } from '@/types' import type { ZigbeeNode, ZigbeeEdge } from '@/components/zigbee/types' import type { ZwaveNode, ZwaveEdge } from '@/components/zwave/types' @@ -62,12 +63,19 @@ const STANDALONE = import.meta.env.VITE_STANDALONE === 'true' export default function App() { const { loadCanvas, applyLayout, markSaved, markUnsaved, hasUnsavedChanges, editSeq, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer, floorMap, setFloorMap } = useCanvasStore() const canvasRef = useRef(null) - const { isAuthenticated } = useAuthStore() + const { isAuthenticated, isInitialized } = useAuthStore() + const authBootstrapStarted = useRef(false) const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore() const { activeDesignId, setDesigns, setActiveDesign } = useDesignStore() useStatusPolling() + useEffect(() => { + if (STANDALONE || authBootstrapStarted.current) return + authBootstrapStarted.current = true + void bootstrapAuth() + }, []) + const [autosave, setAutosave] = useState(readAutosaveSettings) useEffect(() => subscribeAutosaveSettings(setAutosave), []) @@ -922,6 +930,13 @@ export default function App() { const editNode = editNodeId ? nodes.find((n) => n.id === editNodeId) : null const editEdge = editEdgeId ? edges.find((e) => e.id === editEdgeId) : null + if (!STANDALONE && !isInitialized) { + return ( +
+ Loading authentication… +
+ ) + } if (!STANDALONE && !isAuthenticated) return return ( diff --git a/frontend/src/api/__tests__/client.test.ts b/frontend/src/api/__tests__/client.test.ts index 5f3bc66..3cda64d 100644 --- a/frontend/src/api/__tests__/client.test.ts +++ b/frontend/src/api/__tests__/client.test.ts @@ -6,7 +6,7 @@ type Interceptor = { } interface MockInstance { - defaults: { baseURL?: string } + defaults: { baseURL?: string; withCredentials?: boolean } interceptors: { request: { use: (f: Interceptor['fulfilled'], r?: Interceptor['rejected']) => void } response: { use: (f: Interceptor['fulfilled'], r?: Interceptor['rejected']) => void } @@ -25,9 +25,9 @@ const instances = hoisted.instances as MockInstance[] vi.mock('axios', () => { return { default: { - create: (cfg: { baseURL?: string }) => { + create: (cfg: { baseURL?: string; withCredentials?: boolean }) => { const inst: MockInstance = { - defaults: { baseURL: cfg?.baseURL }, + defaults: { baseURL: cfg?.baseURL, withCredentials: cfg?.withCredentials }, interceptors: { request: { use: (f: unknown, r?: unknown) => { inst.__req = { fulfilled: f as never, rejected: r as never } } }, response: { use: (f: unknown, r?: unknown) => { inst.__res = { fulfilled: f as never, rejected: r as never } } }, @@ -54,7 +54,7 @@ describe('api/client', () => { const [api, publicApi] = instances beforeEach(() => { - useAuthStore.setState({ token: null, isAuthenticated: false }) + useAuthStore.setState({ token: null, csrfToken: null, isAuthenticated: false }) api.get.mockClear() api.post.mockClear() api.patch.mockClear() @@ -66,6 +66,7 @@ describe('api/client', () => { it('creates two axios instances with /api/v1 baseURL', () => { expect(instances).toHaveLength(2) expect(api.defaults.baseURL).toBe('/api/v1') + expect(api.defaults.withCredentials).toBe(true) expect(publicApi.defaults.baseURL).toBe('/api/v1') }) @@ -86,6 +87,18 @@ describe('api/client', () => { expect((out as typeof cfg).headers.Authorization).toBeUndefined() }) + it('adds the in-memory OIDC CSRF token only to unsafe requests', () => { + useAuthStore.setState({ csrfToken: 'csrf-123' }) + const post = { method: 'post', headers: {} as Record } + const get = { method: 'get', headers: {} as Record } + + api.__req.fulfilled!(post) + api.__req.fulfilled!(get) + + expect(post.headers['X-Homelable-CSRF']).toBe('csrf-123') + expect(get.headers['X-Homelable-CSRF']).toBeUndefined() + }) + it('response interceptor passes through 2xx responses', () => { const r = { status: 200, data: { ok: true } } expect(api.__res.fulfilled!(r)).toBe(r) @@ -125,6 +138,15 @@ describe('api/client', () => { expect(api.post).toHaveBeenCalledWith('/auth/login', { username: 'u', password: 'p' }) }) + it('authApi discovers auth publicly and manages the current session', () => { + mod.authApi.config() + expect(publicApi.get).toHaveBeenCalledWith('/auth/config') + mod.authApi.me() + expect(api.get).toHaveBeenCalledWith('/auth/me') + mod.authApi.logout() + expect(api.post).toHaveBeenCalledWith('/auth/logout') + }) + it('canvasApi.load GETs /canvas', () => { mod.canvasApi.load() expect(api.get).toHaveBeenCalledWith('/canvas', expect.objectContaining({})) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index d792c0d..abacdf6 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,16 +1,22 @@ import axios from 'axios' import { useAuthStore } from '@/stores/authStore' +import type { AuthMode, AuthUser } from '@/stores/authStore' export const api = axios.create({ baseURL: '/api/v1', + withCredentials: true, }) // Unauthenticated axios instance — no JWT, no 401 redirect (used for public endpoints) const publicApi = axios.create({ baseURL: '/api/v1' }) api.interceptors.request.use((config) => { - const token = useAuthStore.getState().token + const { token, csrfToken } = useAuthStore.getState() if (token) config.headers.Authorization = `Bearer ${token}` + const method = config.method?.toUpperCase() ?? 'GET' + if (csrfToken && !['GET', 'HEAD', 'OPTIONS'].includes(method)) { + config.headers['X-Homelable-CSRF'] = csrfToken + } return config }) @@ -23,8 +29,11 @@ api.interceptors.response.use( ) export const authApi = { + config: () => publicApi.get<{ mode: AuthMode; oidc_login_url: string | null }>('/auth/config'), login: (username: string, password: string) => api.post<{ access_token: string }>('/auth/login', { username, password }), + me: () => api.get('/auth/me'), + logout: () => api.post('/auth/logout'), } export const canvasApi = { diff --git a/frontend/src/auth/__tests__/bootstrap.test.ts b/frontend/src/auth/__tests__/bootstrap.test.ts new file mode 100644 index 0000000..36689ec --- /dev/null +++ b/frontend/src/auth/__tests__/bootstrap.test.ts @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useAuthStore } from '@/stores/authStore' + +vi.mock('@/api/client', () => ({ + authApi: { + config: vi.fn(), + me: vi.fn(), + }, +})) + +import { authApi } from '@/api/client' +import { bootstrapAuth } from '../bootstrap' + +describe('bootstrapAuth', () => { + beforeEach(() => { + sessionStorage.clear() + useAuthStore.setState({ + token: null, + isAuthenticated: false, + isInitialized: false, + authMode: null, + authMethod: null, + oidcLoginUrl: null, + displayName: null, + csrfToken: null, + }) + vi.mocked(authApi.config).mockReset() + vi.mocked(authApi.me).mockReset() + }) + + it('restores an OIDC cookie session without storing an IdP token', async () => { + vi.mocked(authApi.config).mockResolvedValue({ + data: { mode: 'oidc', oidc_login_url: '/api/v1/auth/oidc/login' }, + } as never) + vi.mocked(authApi.me).mockResolvedValue({ + data: { + subject: 'user-1', + display_name: 'Alice', + auth_method: 'oidc', + issuer: 'https://id.example/application/o/homelable/', + csrf_token: 'csrf-123', + }, + } as never) + + await bootstrapAuth() + + const state = useAuthStore.getState() + expect(authApi.me).toHaveBeenCalledOnce() + expect(state.isAuthenticated).toBe(true) + expect(state.authMethod).toBe('oidc') + expect(state.token).toBeNull() + expect(state.csrfToken).toBe('csrf-123') + }) + + it('does not probe /auth/me for local mode without a persisted token', async () => { + vi.mocked(authApi.config).mockResolvedValue({ + data: { mode: 'local', oidc_login_url: null }, + } as never) + + await bootstrapAuth() + + expect(authApi.me).not.toHaveBeenCalled() + expect(useAuthStore.getState()).toMatchObject({ + authMode: 'local', + isAuthenticated: false, + isInitialized: true, + }) + }) + + it('validates and retains a persisted local bearer token', async () => { + useAuthStore.setState({ token: 'local-token' }) + vi.mocked(authApi.config).mockResolvedValue({ + data: { mode: 'local', oidc_login_url: null }, + } as never) + vi.mocked(authApi.me).mockResolvedValue({ + data: { + subject: 'admin', + display_name: 'admin', + auth_method: 'local', + issuer: null, + csrf_token: null, + }, + } as never) + + await bootstrapAuth() + + expect(useAuthStore.getState()).toMatchObject({ + token: 'local-token', + isAuthenticated: true, + authMethod: 'local', + }) + }) + + it('fails closed when the backend rejects the session', async () => { + useAuthStore.setState({ token: 'expired-token', isAuthenticated: true }) + vi.mocked(authApi.config).mockResolvedValue({ + data: { mode: 'local', oidc_login_url: null }, + } as never) + vi.mocked(authApi.me).mockRejectedValue({ response: { status: 401 } }) + + await bootstrapAuth() + + expect(useAuthStore.getState()).toMatchObject({ + token: null, + isAuthenticated: false, + isInitialized: true, + }) + }) + + it('leaves the auth mode unknown when discovery is unavailable', async () => { + vi.mocked(authApi.config).mockRejectedValue(new Error('network down')) + + await bootstrapAuth() + + expect(useAuthStore.getState()).toMatchObject({ + authMode: null, + isAuthenticated: false, + isInitialized: true, + }) + }) +}) diff --git a/frontend/src/auth/bootstrap.ts b/frontend/src/auth/bootstrap.ts new file mode 100644 index 0000000..4b7e92b --- /dev/null +++ b/frontend/src/auth/bootstrap.ts @@ -0,0 +1,33 @@ +import { authApi } from '@/api/client' +import { useAuthStore } from '@/stores/authStore' + +/** + * Discover the configured authentication mode and restore a valid session. + * OIDC sessions are represented only by the backend's HttpOnly cookie; local + * sessions retain their existing bearer token until /auth/me validates it. + */ +export async function bootstrapAuth(): Promise { + const store = useAuthStore.getState() + + try { + const { data: config } = await authApi.config() + store.setAuthConfig(config.mode, config.oidc_login_url) + + const token = useAuthStore.getState().token + if (config.mode === 'oidc' || token) { + try { + const { data: user } = await authApi.me() + useAuthStore.getState().authenticate(user) + } catch { + // The response interceptor clears invalid credentials on 401. Also + // clear them for malformed/unreachable session checks, failing closed. + useAuthStore.getState().logout() + } + } + } catch { + // Keep authMode unknown when discovery is unreachable. The login screen + // reports that state without guessing which authentication mode is active. + } finally { + useAuthStore.getState().finishInitialization() + } +} diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx index 3272798..bdea9f5 100644 --- a/frontend/src/components/LoginPage.tsx +++ b/frontend/src/components/LoginPage.tsx @@ -1,17 +1,18 @@ import { useState } from 'react' import { Network, Loader2 } from 'lucide-react' -import { Button } from '@/components/ui/button' +import { Button, buttonVariants } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { authApi } from '@/api/client' import { useAuthStore } from '@/stores/authStore' +import { cn } from '@/lib/utils' export function LoginPage() { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) - const login = useAuthStore((s) => s.login) + const { authMode, oidcLoginUrl, login } = useAuthStore() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -51,53 +52,82 @@ export function LoginPage() { - {/* Form */} -
-
- - setUsername(e.target.value)} - className="bg-[#21262d] border-[#30363d] focus-visible:ring-[#00d4ff]/50 text-sm" - placeholder="admin" - required - /> + {authMode === 'local' && ( + <> + +
+ + setUsername(e.target.value)} + className="bg-[#21262d] border-[#30363d] focus-visible:ring-[#00d4ff]/50 text-sm" + placeholder="admin" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="bg-[#21262d] border-[#30363d] focus-visible:ring-[#00d4ff]/50 text-sm" + required + /> +
+ + {error && ( +

{error}

+ )} + + + + +

+ Credentials configured in .env +

+ + )} + + {authMode === 'oidc' && ( +
+

+ Continue through the configured OpenID Connect provider. +

+ + Sign in with OpenID Connect +
+ )} -
- - setPassword(e.target.value)} - className="bg-[#21262d] border-[#30363d] focus-visible:ring-[#00d4ff]/50 text-sm" - required - /> + {authMode === null && ( +
+

Could not load authentication configuration.

+
- - {error && ( -

{error}

- )} - - - - -

- Credentials configured in .env -

+ )}
) diff --git a/frontend/src/components/__tests__/LoginPage.test.tsx b/frontend/src/components/__tests__/LoginPage.test.tsx index 64ca1ab..7df6e52 100644 --- a/frontend/src/components/__tests__/LoginPage.test.tsx +++ b/frontend/src/components/__tests__/LoginPage.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { act, render, screen, fireEvent, waitFor } from '@testing-library/react' import { LoginPage } from '../LoginPage' import { useAuthStore } from '@/stores/authStore' @@ -13,7 +13,12 @@ import { authApi } from '@/api/client' describe('LoginPage', () => { beforeEach(() => { - useAuthStore.setState({ token: null, isAuthenticated: false }) + useAuthStore.setState({ + token: null, + isAuthenticated: false, + authMode: 'local', + oidcLoginUrl: null, + }) vi.mocked(authApi.login).mockReset() }) @@ -30,6 +35,30 @@ describe('LoginPage', () => { expect(screen.getByRole('button', { name: /sign in/i })).toBeDefined() }) + it('renders only the provider redirect in OIDC mode', () => { + useAuthStore.setState({ + authMode: 'oidc', + oidcLoginUrl: '/api/v1/auth/oidc/login', + }) + + render() + + expect(screen.queryByLabelText('Username')).toBeNull() + expect(screen.queryByLabelText('Password')).toBeNull() + expect(screen.getByRole('link', { name: 'Sign in with OpenID Connect' })) + .toHaveAttribute('href', '/api/v1/auth/oidc/login') + }) + + it('does not guess an auth mode when discovery failed', () => { + useAuthStore.setState({ authMode: null }) + + render() + + expect(screen.getByText('Could not load authentication configuration.')).toBeDefined() + expect(screen.queryByLabelText('Username')).toBeNull() + expect(screen.getByRole('button', { name: 'Retry' })).toBeDefined() + }) + // ── Security checks ────────────────────────────────────────────────────── it('password field type is "password" — not rendered as plain text', () => { @@ -102,7 +131,9 @@ describe('LoginPage', () => { await waitFor(() => { expect((screen.getByRole('button', { name: '' }) as HTMLButtonElement).disabled).toBe(true) }) - resolve({ data: { access_token: 'tok' } }) + await act(async () => { + resolve({ data: { access_token: 'tok' } }) + }) }) it('calls authApi.login with credentials via POST body (not URL params)', async () => { diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index 45aa1dc..30f285c 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -5,7 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { useCanvasStore } from '@/stores/canvasStore' import { useDesignStore } from '@/stores/designStore' import { useAuthStore } from '@/stores/authStore' -import { designsApi, mediaApi } from '@/api/client' +import { authApi, designsApi, mediaApi } from '@/api/client' import * as standaloneStorage from '@/utils/standaloneStorage' import { resolveDesignIcon, DEFAULT_DESIGN_ICON } from '@/utils/designIcons' import { DesignModal, type DesignFormData } from '@/components/modals/DesignModal' @@ -47,6 +47,15 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee const { nodes, hasUnsavedChanges, floorMap, setFloorMap } = useCanvasStore() const floorMapEditNonce = useCanvasStore((s) => s.floorMapEditNonce) + const handleLogout = useCallback(async () => { + try { + await authApi.logout() + logout() + } catch { + toast.error('Could not end the current session') + } + }, [logout]) + const openDesignModal = useCallback((m: { mode: 'create' | 'edit'; design?: Design }) => { setOpenSeq((s) => s + 1) setDesignModal(m) @@ -294,7 +303,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee icon={LogOut} label="Logout" collapsed={collapsed} - onClick={logout} + onClick={() => void handleLogout()} /> )} diff --git a/frontend/src/components/panels/__tests__/Sidebar.test.tsx b/frontend/src/components/panels/__tests__/Sidebar.test.tsx index c74e73d..45e11b6 100644 --- a/frontend/src/components/panels/__tests__/Sidebar.test.tsx +++ b/frontend/src/components/panels/__tests__/Sidebar.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { Sidebar } from '../Sidebar' import { useCanvasStore } from '@/stores/canvasStore' import { useAuthStore } from '@/stores/authStore' @@ -13,6 +13,9 @@ vi.mock('@/stores/canvasStore') vi.mock('@/stores/authStore') vi.mock('@/api/client', () => ({ + authApi: { + logout: vi.fn().mockResolvedValue({}), + }, scanApi: { trigger: vi.fn().mockResolvedValue({}), runs: vi.fn().mockResolvedValue({ data: [] }), @@ -272,10 +275,14 @@ describe('Sidebar', () => { expect(screen.getByText('Logout')).toBeInTheDocument() }) - it('calls logout when Logout is clicked', () => { + it('ends the backend session before clearing local auth state', async () => { + const { authApi } = await import('@/api/client') render() fireEvent.click(screen.getByText('Logout')) - expect(mockLogout).toHaveBeenCalledOnce() + await waitFor(() => { + expect(authApi.logout).toHaveBeenCalledOnce() + expect(mockLogout).toHaveBeenCalledOnce() + }) }) }) diff --git a/frontend/src/hooks/__tests__/useStatusPolling.test.ts b/frontend/src/hooks/__tests__/useStatusPolling.test.ts index 1a89280..a558b42 100644 --- a/frontend/src/hooks/__tests__/useStatusPolling.test.ts +++ b/frontend/src/hooks/__tests__/useStatusPolling.test.ts @@ -39,6 +39,7 @@ describe('useStatusPolling', () => { vi.mocked(useAuthStore).mockReturnValue({ isAuthenticated: true, + authMethod: 'local', token: 'test-token', } as ReturnType) @@ -58,6 +59,7 @@ describe('useStatusPolling', () => { it('does not open WebSocket when not authenticated', () => { vi.mocked(useAuthStore).mockReturnValue({ isAuthenticated: false, + authMethod: null, token: null, } as ReturnType) renderHook(() => useStatusPolling()) @@ -67,12 +69,28 @@ describe('useStatusPolling', () => { it('does not open WebSocket when token is missing', () => { vi.mocked(useAuthStore).mockReturnValue({ isAuthenticated: true, + authMethod: 'local', token: null, } as ReturnType) renderHook(() => useStatusPolling()) expect(MockWebSocket.instances).toHaveLength(0) }) + it('uses the HttpOnly cookie and sends no credential in OIDC mode', () => { + vi.mocked(useAuthStore).mockReturnValue({ + isAuthenticated: true, + authMethod: 'oidc', + token: null, + } as ReturnType) + + renderHook(() => useStatusPolling()) + + expect(MockWebSocket.instances).toHaveLength(1) + const ws = MockWebSocket.instances[0] + ws.onopen?.() + expect(ws.send).not.toHaveBeenCalled() + }) + it('connects to correct ws:// URL', () => { renderHook(() => useStatusPolling()) expect(MockWebSocket.instances).toHaveLength(1) diff --git a/frontend/src/hooks/useStatusPolling.ts b/frontend/src/hooks/useStatusPolling.ts index b21b340..7b59c39 100644 --- a/frontend/src/hooks/useStatusPolling.ts +++ b/frontend/src/hooks/useStatusPolling.ts @@ -25,10 +25,10 @@ const STANDALONE = import.meta.env.VITE_STANDALONE === 'true' export function useStatusPolling() { const wsRef = useRef(null) const { setNodeStatus, notifyScanDeviceFound, setServiceStatuses } = useCanvasStore() - const { isAuthenticated, token } = useAuthStore() + const { isAuthenticated, authMethod, token } = useAuthStore() useEffect(() => { - if (STANDALONE || !isAuthenticated || !token) return + if (STANDALONE || !isAuthenticated || (authMethod !== 'oidc' && !token)) return const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' const host = window.location.host // includes port when non-standard @@ -37,9 +37,13 @@ export function useStatusPolling() { const ws = new WebSocket(url) wsRef.current = ws - // Send token as first message (not in URL to avoid log/history exposure) - ws.onopen = () => { - ws.send(JSON.stringify({ token })) + // Local mode sends the bearer token as the first message (not in the URL + // where proxies may log it). OIDC mode is authenticated during the upgrade + // with the backend's HttpOnly session cookie, so React sends no credential. + if (token) { + ws.onopen = () => { + ws.send(JSON.stringify({ token })) + } } ws.onmessage = (event) => { @@ -71,5 +75,5 @@ export function useStatusPolling() { ws.close() wsRef.current = null } - }, [isAuthenticated, token, setNodeStatus, notifyScanDeviceFound, setServiceStatuses]) + }, [isAuthenticated, authMethod, token, setNodeStatus, notifyScanDeviceFound, setServiceStatuses]) } diff --git a/frontend/src/stores/__tests__/authStore.test.ts b/frontend/src/stores/__tests__/authStore.test.ts index b44e453..1dde8a6 100644 --- a/frontend/src/stores/__tests__/authStore.test.ts +++ b/frontend/src/stores/__tests__/authStore.test.ts @@ -4,7 +4,17 @@ import { useAuthStore } from '@/stores/authStore' describe('authStore', () => { beforeEach(() => { localStorage.clear() - useAuthStore.setState({ token: null, isAuthenticated: false }) + sessionStorage.clear() + useAuthStore.setState({ + token: null, + isAuthenticated: false, + isInitialized: false, + authMode: null, + authMethod: null, + oidcLoginUrl: null, + displayName: null, + csrfToken: null, + }) }) it('starts unauthenticated', () => { @@ -18,6 +28,8 @@ describe('authStore', () => { const { token, isAuthenticated } = useAuthStore.getState() expect(token).toBe('my-jwt-token') expect(isAuthenticated).toBe(true) + expect(useAuthStore.getState().authMethod).toBe('local') + expect(useAuthStore.getState().isInitialized).toBe(true) }) it('logout clears token and isAuthenticated', () => { @@ -27,4 +39,36 @@ describe('authStore', () => { expect(token).toBeNull() expect(isAuthenticated).toBe(false) }) + + it('stores OIDC identity and CSRF state without a browser-readable credential', () => { + useAuthStore.getState().setAuthConfig('oidc', '/api/v1/auth/oidc/login') + useAuthStore.getState().authenticate({ + subject: 'user-1', + display_name: 'Alice', + auth_method: 'oidc', + issuer: 'https://id.example/application/o/homelable/', + csrf_token: 'csrf-123', + }) + + const state = useAuthStore.getState() + expect(state.token).toBeNull() + expect(state.isAuthenticated).toBe(true) + expect(state.authMethod).toBe('oidc') + expect(state.displayName).toBe('Alice') + expect(state.csrfToken).toBe('csrf-123') + expect(sessionStorage.getItem('homelable-auth') ?? '').not.toContain('csrf-123') + }) + + it('keeps a validated local token when applying /auth/me identity', () => { + useAuthStore.getState().login('local-token') + useAuthStore.getState().authenticate({ + subject: 'admin', + display_name: 'admin', + auth_method: 'local', + issuer: null, + csrf_token: null, + }) + + expect(useAuthStore.getState().token).toBe('local-token') + }) }) diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts index 889ff88..8f44ea5 100644 --- a/frontend/src/stores/authStore.ts +++ b/frontend/src/stores/authStore.ts @@ -1,9 +1,29 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' +export type AuthMode = 'local' | 'oidc' +export type AuthMethod = AuthMode + +export interface AuthUser { + subject: string + display_name: string + auth_method: AuthMethod + issuer: string | null + csrf_token: string | null +} + interface AuthState { token: string | null isAuthenticated: boolean + isInitialized: boolean + authMode: AuthMode | null + authMethod: AuthMethod | null + oidcLoginUrl: string | null + displayName: string | null + csrfToken: string | null + setAuthConfig: (mode: AuthMode, oidcLoginUrl: string | null) => void + authenticate: (user: AuthUser) => void + finishInitialization: () => void login: (token: string) => void logout: () => void } @@ -13,14 +33,56 @@ export const useAuthStore = create()( (set) => ({ token: null, isAuthenticated: false, - login: (token) => set({ token, isAuthenticated: true }), - logout: () => set({ token: null, isAuthenticated: false }), + isInitialized: false, + authMode: null, + authMethod: null, + oidcLoginUrl: null, + displayName: null, + csrfToken: null, + setAuthConfig: (authMode, oidcLoginUrl) => set({ authMode, oidcLoginUrl }), + authenticate: (user) => set((state) => ({ + token: user.auth_method === 'oidc' ? null : state.token, + isAuthenticated: true, + isInitialized: true, + authMethod: user.auth_method, + displayName: user.display_name, + csrfToken: user.csrf_token, + })), + finishInitialization: () => set({ isInitialized: true }), + login: (token) => set({ + token, + isAuthenticated: true, + isInitialized: true, + authMethod: 'local', + displayName: null, + csrfToken: null, + }), + logout: () => set({ + token: null, + isAuthenticated: false, + isInitialized: true, + authMethod: null, + displayName: null, + csrfToken: null, + }), }), { name: 'homelable-auth', // sessionStorage: scoped to the tab, cleared on browser close. // Prevents XSS from other tabs stealing the token via localStorage. storage: createJSONStorage(() => sessionStorage), + // OIDC credentials stay in HttpOnly cookies. Its CSRF token is deliberately + // memory-only and is recovered from /auth/me after every page load. + partialize: (state) => ({ token: state.token }), + // Ignore authentication flags written by older versions of the store. A + // persisted local token is revalidated by the bootstrap request instead. + merge: (persisted, current) => { + const token = (persisted as { token?: unknown } | null)?.token + return { + ...current, + token: typeof token === 'string' ? token : null, + } + }, } ) ) From f061db645b89aa481acb564ea25f722e3d487057 Mon Sep 17 00:00:00 2001 From: ypso Date: Tue, 21 Jul 2026 00:36:06 +0200 Subject: [PATCH 3/8] chore(deps): refresh vulnerable frontend packages --- frontend/package-lock.json | 183 +++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 78 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f31c319..647a8d4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2520,9 +2520,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2537,9 +2534,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2554,9 +2548,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2571,9 +2562,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2588,9 +2576,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2605,9 +2590,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2622,9 +2604,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2639,9 +2618,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2656,9 +2632,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2673,9 +2646,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2690,9 +2660,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2707,9 +2674,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2724,9 +2688,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2975,9 +2936,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2995,9 +2953,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3015,9 +2970,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3035,9 +2987,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3077,6 +3026,70 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", @@ -3237,9 +3250,9 @@ } }, "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3651,9 +3664,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4186,16 +4199,42 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4250,9 +4289,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6638,9 +6677,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -6945,9 +6984,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6969,9 +7005,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6993,9 +7026,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7017,9 +7047,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ From 99e7b9a343beb6e9d0c208e2eb4fbafa86d4f284 Mon Sep 17 00:00:00 2001 From: ypso Date: Tue, 21 Jul 2026 00:37:32 +0200 Subject: [PATCH 4/8] ci(security): enforce unsuppressed Python audit --- .github/workflows/security.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3118aec..3be3184 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -36,6 +36,4 @@ jobs: with: python-version: '3.11' - name: Pip audit - # CVE-2024-23342 (ecdsa): timing attack on ECDSA — not exploitable (app uses HS256/HMAC only). - # python-jose forces ecdsa as a dependency with no fixed version available. - run: pip install pip-audit && pip-audit -r backend/requirements.txt --ignore-vuln CVE-2024-23342 + run: pip install pip-audit && pip-audit -r backend/requirements.txt From 37ad9c6933f79a37fe2af28b2d270d7b86c8b69c Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Jul 2026 23:34:34 +0200 Subject: [PATCH 5/8] fix(ci): scope dependency-audit npm audit to runtime deps The dependency-audit job failed on vulnerabilities from `shadcn` and its transitive tree (hono, fast-uri, body-parser, @modelcontextprotocol/sdk). `shadcn` is a build-time scaffolding CLI plus a CSS `@import`, never shipped in the browser bundle, so it belongs in devDependencies. - move `shadcn` to devDependencies (regenerated lockfile: dev flags only, no version changes) - run `npm audit` with `--omit=dev`, matching the runtime-only intent of the pip-audit step npm audit --omit=dev --audit-level=high now reports 0 vulnerabilities; the production build still resolves the shadcn CSS import. ha-relevant: no --- .github/workflows/security.yml | 5 +- frontend/package-lock.json | 343 ++++++++++++++++++++++++++++++++- frontend/package.json | 2 +- 3 files changed, 344 insertions(+), 6 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3be3184..7c056eb 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -31,7 +31,10 @@ jobs: with: node-version: '20' - name: NPM Audit - run: cd frontend && npm audit --audit-level=high + # --omit=dev: only ship-time deps gate the build. Dev-only tooling + # (shadcn CLI and its transitive tree: hono, fast-uri, …) is never + # bundled or served, so its advisories must not fail release CI. + run: cd frontend && npm audit --omit=dev --audit-level=high - uses: actions/setup-python@v5 with: python-version: '3.11' diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 647a8d4..26b1964 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,6 @@ "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "shadcn": "^4.0.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", @@ -49,6 +48,7 @@ "globals": "^16.5.0", "jsdom": "^28.1.0", "lucide-react": "^1.7.0", + "shadcn": "^4.0.0", "tailwindcss": "^4.2.1", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", @@ -121,6 +121,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -135,6 +136,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -144,6 +146,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -174,6 +177,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -190,6 +194,7 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -202,6 +207,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.7", @@ -218,6 +224,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -239,6 +246,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -248,6 +256,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.5", @@ -261,6 +270,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -274,6 +284,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", @@ -291,6 +302,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -303,6 +315,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -312,6 +325,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", @@ -329,6 +343,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -342,6 +357,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -351,6 +367,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -360,6 +377,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -369,6 +387,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", @@ -382,6 +401,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -397,6 +417,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -412,6 +433,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -427,6 +449,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -475,6 +498,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -494,6 +518,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -522,6 +547,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -536,6 +562,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -554,6 +581,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -798,6 +826,7 @@ "version": "1.60.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.60.1.tgz", "integrity": "sha512-pPKqhE/HiaPDfbSf6doJnxeqzLszWP4eLICB89wRDZGaBaLzGpa3RgahVYIauBonaEWT8oxqAyacWKHtD+n3hQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "commander": "^11.1.0", @@ -822,6 +851,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -831,6 +861,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -854,6 +885,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -866,6 +898,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -875,6 +908,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -887,6 +921,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -896,6 +931,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -908,6 +944,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -923,12 +960,14 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -938,6 +977,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -953,6 +993,7 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "dev": true, "license": "MIT", "engines": { "bun": ">=1", @@ -1649,6 +1690,7 @@ "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1713,6 +1755,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -1722,6 +1765,7 @@ "version": "5.1.21", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -1743,6 +1787,7 @@ "version": "10.3.2", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -1770,6 +1815,7 @@ "version": "1.0.15", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -1779,6 +1825,7 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -1796,6 +1843,7 @@ "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", @@ -1806,6 +1854,7 @@ "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", @@ -1816,6 +1865,7 @@ "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" @@ -1825,12 +1875,14 @@ "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", @@ -1841,6 +1893,7 @@ "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -1881,6 +1934,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1897,12 +1951,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/@mswjs/interceptors": { "version": "0.41.3", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", + "dev": true, "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", @@ -1920,6 +1976,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -1932,6 +1989,7 @@ "version": "1.9.7", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -1947,6 +2005,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -1959,6 +2018,7 @@ "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", @@ -1972,6 +2032,7 @@ "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" @@ -1981,6 +2042,7 @@ "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", @@ -1994,12 +2056,14 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", @@ -2010,6 +2074,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, "license": "MIT" }, "node_modules/@radix-ui/primitive": { @@ -2782,12 +2847,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, "license": "MIT" }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3233,6 +3300,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, "license": "MIT", "dependencies": { "fast-glob": "^3.3.3", @@ -3244,6 +3312,7 @@ "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" @@ -3253,6 +3322,7 @@ "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3265,6 +3335,7 @@ "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" @@ -3427,7 +3498,7 @@ "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -3457,12 +3528,14 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, "license": "MIT" }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -3989,6 +4062,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -4002,6 +4076,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4011,6 +4086,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -4050,6 +4126,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -4076,6 +4153,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -4093,6 +4171,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4109,12 +4188,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4124,6 +4205,7 @@ "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" @@ -4165,6 +4247,7 @@ "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -4246,6 +4329,7 @@ "version": "2.10.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4268,6 +4352,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -4303,6 +4388,7 @@ "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" @@ -4315,6 +4401,7 @@ "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", @@ -4348,6 +4435,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -4363,6 +4451,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -4385,6 +4474,7 @@ "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", @@ -4401,6 +4491,7 @@ "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" @@ -4410,6 +4501,7 @@ "version": "1.0.30001786", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4475,6 +4567,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" @@ -4490,6 +4583,7 @@ "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4502,6 +4596,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, "license": "ISC", "engines": { "node": ">= 12" @@ -4511,6 +4606,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -4525,12 +4621,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4545,6 +4643,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4557,6 +4656,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -4583,12 +4683,14 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4601,6 +4703,7 @@ "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/combined-stream": { @@ -4619,6 +4722,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -4635,6 +4739,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4648,6 +4753,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4657,12 +4763,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4672,6 +4780,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.6.0" @@ -4681,6 +4790,7 @@ "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -4698,6 +4808,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.1", @@ -4724,6 +4835,7 @@ "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", @@ -4759,6 +4871,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -4919,6 +5032,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -4966,6 +5080,7 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -4987,6 +5102,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4996,6 +5112,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -5012,6 +5129,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -5024,6 +5142,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5045,6 +5164,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5074,6 +5194,7 @@ "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -5091,6 +5212,7 @@ "version": "17.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5117,6 +5239,7 @@ "version": "0.4.18", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "dev": true, "license": "MIT", "dependencies": { "@ecies/ciphers": "^0.2.5", @@ -5134,24 +5257,28 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.331", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5188,6 +5315,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5197,6 +5325,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5300,6 +5429,7 @@ "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" @@ -5309,6 +5439,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -5466,6 +5597,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -5535,6 +5667,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5544,6 +5677,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -5556,6 +5690,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -5565,6 +5700,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -5601,6 +5737,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -5644,6 +5781,7 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.2.0" @@ -5662,6 +5800,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5671,6 +5810,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -5687,12 +5827,14 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5709,6 +5851,7 @@ "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" @@ -5735,6 +5878,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, "funding": [ { "type": "github", @@ -5751,6 +5895,7 @@ "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" @@ -5760,6 +5905,7 @@ "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" @@ -5777,6 +5923,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, "funding": [ { "type": "github", @@ -5800,6 +5947,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" @@ -5828,6 +5976,7 @@ "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" @@ -5840,6 +5989,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -5935,6 +6085,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -5947,6 +6098,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5956,6 +6108,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5965,6 +6118,7 @@ "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -6003,12 +6157,14 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, "license": "MIT" }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -6018,6 +6174,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6027,6 +6184,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6063,6 +6221,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "dev": true, "license": "MIT", "engines": { "node": ">=14.16" @@ -6088,6 +6247,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, "license": "MIT", "dependencies": { "@sec-ant/readable-stream": "^0.4.1", @@ -6142,6 +6302,7 @@ "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/graphlib": { @@ -6157,6 +6318,7 @@ "version": "16.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -6215,6 +6377,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, "license": "MIT" }, "node_modules/hermes-estree": { @@ -6238,6 +6401,7 @@ "version": "4.12.25", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -6273,6 +6437,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -6307,6 +6472,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -6320,6 +6486,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -6329,6 +6496,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6345,6 +6513,7 @@ "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" @@ -6354,6 +6523,7 @@ "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", @@ -6390,12 +6560,14 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -6405,6 +6577,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -6414,12 +6587,14 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -6435,6 +6610,7 @@ "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" @@ -6444,6 +6620,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6453,6 +6630,7 @@ "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" @@ -6465,6 +6643,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -6477,6 +6656,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -6495,6 +6675,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6507,12 +6688,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6522,6 +6705,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6534,6 +6718,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6553,12 +6738,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, "license": "MIT" }, "node_modules/is-regexp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6571,6 +6758,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6583,6 +6771,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6595,6 +6784,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -6610,6 +6800,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -6665,6 +6856,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -6674,6 +6866,7 @@ "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": { @@ -6743,6 +6936,7 @@ "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" @@ -6762,6 +6956,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -6775,6 +6970,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -6788,6 +6984,7 @@ "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" @@ -6800,6 +6997,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -6822,6 +7020,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7106,6 +7305,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -7141,6 +7341,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -7157,6 +7358,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -7169,6 +7371,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7181,6 +7384,7 @@ "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" @@ -7278,6 +7482,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7287,6 +7492,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7299,12 +7505,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -7314,6 +7522,7 @@ "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", @@ -7327,6 +7536,7 @@ "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" @@ -7360,6 +7570,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7369,6 +7580,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7404,6 +7616,7 @@ "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" @@ -7419,6 +7632,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/msw/-/msw-2.13.0.tgz", "integrity": "sha512-5PPWf7I7DBHb4ZUZ0NUI+/VBDk/eiNYDNJZGt/jZ7+rbCSIK5hRcNTGqWMnn0vT6NrHiQlb0nfpenVGz1vrqpg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -7463,6 +7677,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7476,6 +7691,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" @@ -7485,6 +7701,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -7510,6 +7727,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7530,6 +7748,7 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", + "dev": true, "funding": [ { "type": "github", @@ -7549,6 +7768,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -7567,12 +7787,14 @@ "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, "license": "MIT" }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^4.0.0", @@ -7589,6 +7811,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7601,6 +7824,7 @@ "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" @@ -7610,6 +7834,7 @@ "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" @@ -7622,6 +7847,7 @@ "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -7642,6 +7868,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -7654,6 +7881,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7663,6 +7891,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" @@ -7678,6 +7907,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.4.0", @@ -7716,6 +7946,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -7739,6 +7970,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -7751,6 +7983,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, "license": "MIT" }, "node_modules/p-limit": { @@ -7789,6 +8022,7 @@ "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" @@ -7801,6 +8035,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -7819,6 +8054,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7844,6 +8080,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7853,6 +8090,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -7869,6 +8107,7 @@ "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" @@ -7878,6 +8117,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, "license": "MIT" }, "node_modules/pathe": { @@ -7891,12 +8131,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.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" @@ -7909,6 +8151,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -7918,6 +8161,7 @@ "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", @@ -7946,6 +8190,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -7959,6 +8204,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -8011,6 +8257,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" @@ -8026,6 +8273,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -8039,6 +8287,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8048,6 +8297,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -8080,6 +8330,7 @@ "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -8095,6 +8346,7 @@ "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", @@ -8115,6 +8367,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8124,6 +8377,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -8178,6 +8432,7 @@ "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, "license": "MIT", "dependencies": { "ast-types": "^0.16.1", @@ -8208,6 +8463,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8217,6 +8473,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8232,6 +8489,7 @@ "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" @@ -8241,6 +8499,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, "license": "MIT", "dependencies": { "onetime": "^7.0.0", @@ -8257,12 +8516,14 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", + "dev": true, "license": "MIT" }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -8318,6 +8579,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -8334,6 +8596,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -8344,6 +8607,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8356,6 +8620,7 @@ "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", @@ -8379,6 +8644,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/saxes": { @@ -8404,6 +8670,7 @@ "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" @@ -8413,6 +8680,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -8439,6 +8707,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8448,6 +8717,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -8464,6 +8734,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -8483,12 +8754,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shadcn": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.1.2.tgz", "integrity": "sha512-qNQcCavkbYsgBj+X09tF2bTcwRd8abR880bsFkDU2kMqceMCLAm5c+cLg7kWDhfh1H9g08knpQ5ZEf6y/co16g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -8534,6 +8807,7 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -8543,6 +8817,7 @@ "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" @@ -8555,6 +8830,7 @@ "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" @@ -8564,6 +8840,7 @@ "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", @@ -8583,6 +8860,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8599,6 +8877,7 @@ "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", @@ -8617,6 +8896,7 @@ "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", @@ -8643,6 +8923,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8655,6 +8936,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, "license": "MIT" }, "node_modules/sonner": { @@ -8671,6 +8953,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8680,6 +8963,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8696,6 +8980,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8712,6 +8997,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8724,12 +9010,14 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, "license": "MIT" }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -8747,6 +9035,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-keys": "^1.0.0", @@ -8764,6 +9053,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -8779,6 +9069,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8791,6 +9082,7 @@ "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" @@ -8800,6 +9092,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8864,6 +9157,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -8907,6 +9201,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, "license": "MIT" }, "node_modules/tinybench": { @@ -8957,6 +9252,7 @@ "version": "7.0.28", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "dev": true, "license": "MIT", "dependencies": { "tldts-core": "^7.0.28" @@ -8969,12 +9265,14 @@ "version": "7.0.28", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "dev": true, "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8987,6 +9285,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -8996,6 +9295,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" @@ -9034,6 +9334,7 @@ "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dev": true, "license": "MIT", "dependencies": { "@ts-morph/common": "~0.27.0", @@ -9044,6 +9345,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, "license": "MIT", "dependencies": { "json5": "^2.2.2", @@ -9058,6 +9360,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/tw-animate-css": { @@ -9086,6 +9389,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -9101,6 +9405,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -9115,6 +9420,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9124,6 +9430,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -9140,7 +9447,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -9188,13 +9495,14 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9207,6 +9515,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -9216,6 +9525,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9225,6 +9535,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/kettanaito" @@ -9234,6 +9545,7 @@ "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", @@ -9283,12 +9595,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-name": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, "license": "ISC", "engines": { "node": "^20.17.0 || >=22.9.0" @@ -9298,6 +9612,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9477,6 +9792,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -9521,6 +9837,7 @@ "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" @@ -9563,6 +9880,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9577,12 +9895,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9597,6 +9917,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9609,12 +9930,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, "license": "MIT", "dependencies": { "is-wsl": "^3.1.0", @@ -9648,6 +9971,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -9657,12 +9981,14 @@ "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/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -9681,6 +10007,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -9690,12 +10017,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9710,6 +10039,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9735,6 +10065,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.1.0.tgz", "integrity": "sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==", + "dev": true, "license": "MIT", "dependencies": { "yoctocolors": "^2.1.1" @@ -9750,6 +10081,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9762,6 +10094,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9774,6 +10107,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -9783,6 +10117,7 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" diff --git a/frontend/package.json b/frontend/package.json index 4f7dff5..84289f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,6 @@ "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "shadcn": "^4.0.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", @@ -60,6 +59,7 @@ "globals": "^16.5.0", "jsdom": "^28.1.0", "lucide-react": "^1.7.0", + "shadcn": "^4.0.0", "tailwindcss": "^4.2.1", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", From 9f974f2fcfe6ed527d880359e60acb30bd775d70 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Jul 2026 23:35:50 +0200 Subject: [PATCH 6/8] docs(auth): document OIDC setup and configuration - FEATURES.md: add "Authentication (Local / OpenID Connect)" section + TOC - docs/oidc-auth.md: flow overview, env var reference, fail-closed validation rules, provider setup (Authentik/Keycloak/Authelia/Google), reverse-proxy notes, session/logout caveats, troubleshooting, revert-to-local ha-relevant: no --- FEATURES.md | 18 +++++ docs/oidc-auth.md | 192 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 docs/oidc-auth.md diff --git a/FEATURES.md b/FEATURES.md index eb68124..96d90dd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -29,6 +29,7 @@ Here's what Homelable can do. One line on what each feature is, then how to swit 15. [Gethomepage Widget](#15-gethomepage-widget-) 16. [MCP Server (AI integration)](#16-mcp-server-) 17. [Settings & Shortcuts](#17-settings--shortcuts) +18. [Authentication (Local / OpenID Connect)](#18-authentication-local--openid-connect-) --- @@ -252,4 +253,21 @@ The AI can list nodes/edges/canvas/pending/scans, add/update/delete nodes and ed --- +## 18. Authentication (Local / OpenID Connect) 🔒 + +**What:** Homelable protects the app behind a login. Two exclusive modes, set once in `.env` with `AUTH_MODE`: +- **`local`** (default) — a single username + bcrypt-hashed password. Nothing changes for existing installs. +- **`oidc`** — sign in through your own identity provider (Authentik, Keycloak, Authelia, Google, …) via **Authorization Code + PKCE**. Good for SSO, per-user accounts at the IdP, MFA, and central account management. + +**Use (OIDC):** +1. Register Homelable as a **confidential** client at your IdP and set the redirect URI to `https:///api/v1/auth/oidc/callback`. +2. In `.env` set `AUTH_MODE=oidc` and the `OIDC_*` values (discovery URL, client id/secret, redirect URI, scopes), keep `CORS_ORIGINS` pinned to your browser origin, and give `SECRET_KEY` at least 32 bytes. +3. Restart the backend. The login screen now shows **Sign in with OpenID Connect**. + +**How it protects you:** provider tokens are exchanged server-side and never touch the browser — Homelable issues its own short-lived, `__Host-`, HttpOnly, `SameSite=Lax` session cookie. Discovery metadata, issuer, audience, signature, expiry, nonce, state and PKCE are all validated; cookie-authenticated writes require a CSRF token and an allowed `Origin`; the status WebSocket authenticates from the cookie and validates `Origin`. Local Bearer auth and the MCP service key keep working unchanged. Full config, provider examples and troubleshooting: [docs/oidc-auth.md](./docs/oidc-auth.md). + +> OIDC is Full-mode only — the no-backend standalone build has no login. + +--- + *Installing (Docker, Proxmox LXC, source) is covered in [INSTALLATION.md](./INSTALLATION.md). Running Home Assistant? See [homelable-hacs](https://github.com/Pouzor/homelable-hacs).* diff --git a/docs/oidc-auth.md b/docs/oidc-auth.md new file mode 100644 index 0000000..411a8a4 --- /dev/null +++ b/docs/oidc-auth.md @@ -0,0 +1,192 @@ +# OpenID Connect (OIDC) Authentication + +Homelable can delegate login to your own identity provider (IdP) instead of the +built-in username/password. Sign-in uses the standard **Authorization Code flow +with PKCE**: the browser is redirected to your IdP, and on return Homelable +issues its own short-lived session cookie. Provider access/ID tokens are +exchanged server-side and **never** reach the browser. + +Use it when you want single sign-on, per-user accounts managed at the IdP, MFA, +or central account revocation — Authentik, Keycloak, Authelia, Zitadel, Google, +Microsoft Entra ID, Okta, or any spec-compliant OpenID Connect provider. + +> 🔒 **Server-dependent feature** — requires the Homelable backend. It is hidden +> in the no-backend standalone/demo build, which has no login at all. + +--- + +## How it works + +1. Homelable exposes its mode at `GET /api/v1/auth/config`. In OIDC mode the + login screen shows **Sign in with OpenID Connect**. +2. `GET /api/v1/auth/oidc/login` redirects to your IdP with `state`, `nonce`, + and a PKCE `code_challenge` (S256). +3. The IdP authenticates the user and redirects back to + `GET /api/v1/auth/oidc/callback`. +4. Homelable validates the response (signature via the IdP's JWKS, issuer, + audience, expiry, `nonce`, `state`, PKCE) and mints its **own** session token. +5. That token is stored in a `__Host-`, `HttpOnly`, `Secure`, `SameSite=Lax` + cookie. The browser never sees the provider tokens. + +The display name shown in the app is the first present of +`preferred_username` → `name` → `email` → `sub`. + +Local Bearer authentication and the MCP service key keep working unchanged, so +the API and the MCP server are unaffected. + +--- + +## Prerequisites + +1. A reachable **OpenID Connect** provider with a discovery document + (`.well-known/openid-configuration`). +2. Homelable served over **HTTPS** in production (required for the secure + session cookie). +3. A **confidential** OIDC client (client id **and** secret) registered at the + IdP, with Homelable's callback URL allow-listed as a redirect URI. + +--- + +## Configuration + +All settings live in `.env` (see `.env.example`). Switching to OIDC is +exclusive — it replaces local login. Existing installs stay on +`AUTH_MODE=local` until you change this. + +```env +AUTH_MODE=oidc + +# Pin CORS to the exact browser origin(s). Wildcard is rejected in OIDC mode. +CORS_ORIGINS=["https://homelable.example"] + +# From your IdP: +OIDC_DISCOVERY_URL=https://idp.example/application/o/homelable/.well-known/openid-configuration +OIDC_CLIENT_ID=homelable +OIDC_CLIENT_SECRET=replace-with-a-secret + +# Must EXACTLY match a redirect URI registered at the IdP. +OIDC_REDIRECT_URI=https://homelable.example/api/v1/auth/oidc/callback + +OIDC_SCOPES="openid profile email" +OIDC_COOKIE_SECURE=true +OIDC_SESSION_EXPIRE_MINUTES=480 +OIDC_TRANSACTION_EXPIRE_SECONDS=600 +``` + +Then restart the backend (`docker compose restart backend`). + +### Reference + +| Variable | Required | Default | Notes | +|---|---|---|---| +| `AUTH_MODE` | yes | `local` | Set to `oidc` to enable. | +| `OIDC_DISCOVERY_URL` | yes | — | The IdP's `.well-known/openid-configuration` URL. | +| `OIDC_CLIENT_ID` | yes | — | Confidential client id. | +| `OIDC_CLIENT_SECRET` | yes | — | Client secret. Keep it out of Git and Compose YAML. | +| `OIDC_REDIRECT_URI` | yes | — | Must equal the registered redirect **exactly**, path included. | +| `OIDC_SCOPES` | no | `openid profile email` | Must contain `openid`. | +| `OIDC_COOKIE_SECURE` | no | `true` | `true` requires an HTTPS redirect URI and enables the `__Host-` cookie prefix. Set `false` only for local HTTP testing. | +| `OIDC_SESSION_EXPIRE_MINUTES` | no | `480` | App session lifetime (5–1440). | +| `OIDC_TRANSACTION_EXPIRE_SECONDS` | no | `600` | Lifetime of the short login-flow cookie holding `state`/`nonce`/PKCE (60–3600). | +| `SECRET_KEY` | yes | — | Must be **≥ 32 bytes** in OIDC mode. Generate: `python3 -c "import secrets; print(secrets.token_hex(32))"`. | +| `CORS_ORIGINS` | yes | — | Exact browser origin(s). `*` is rejected in OIDC mode. | + +### Fail-closed validation + +The backend refuses to start in OIDC mode unless the configuration is complete +and safe. It errors if any of the following is true: + +- any of `OIDC_DISCOVERY_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, + `OIDC_REDIRECT_URI` is missing; +- `SECRET_KEY` is shorter than 32 bytes; +- `OIDC_SCOPES` does not contain `openid`; +- `OIDC_DISCOVERY_URL` or `OIDC_REDIRECT_URI` is not an HTTP(S) URL; +- `OIDC_COOKIE_SECURE=true` but `OIDC_REDIRECT_URI` is not HTTPS; +- `CORS_ORIGINS` contains `*`. + +--- + +## Provider setup + +The callback URL to register everywhere is: + +``` +https:///api/v1/auth/oidc/callback +``` + +### Authentik + +1. **Applications → Providers → Create → OAuth2/OpenID Provider.** +2. Client type **Confidential**; add the redirect URI above. +3. Note the client id/secret and the discovery URL: + `https://authentik.example/application/o//.well-known/openid-configuration`. +4. Create an **Application** bound to that provider. + +### Keycloak + +1. In your realm: **Clients → Create client** (OpenID Connect), **Client + authentication ON** (confidential). +2. Set **Valid redirect URIs** to the callback URL. +3. Discovery URL: + `https://kc.example/realms//.well-known/openid-configuration`. +4. Copy the secret from the **Credentials** tab. + +### Authelia + +1. Add Homelable to the `identity_providers.oidc.clients` list with a + `client_secret`, the redirect URI, and scopes `openid profile email`. +2. Discovery URL: `https://auth.example/.well-known/openid-configuration`. + +### Google + +1. **Google Cloud Console → APIs & Services → Credentials → OAuth client ID** + (type *Web application*). +2. Add the callback URL under **Authorized redirect URIs** (Google requires + HTTPS). +3. Discovery URL: `https://accounts.google.com/.well-known/openid-configuration`. + +Any other spec-compliant provider works the same way — point +`OIDC_DISCOVERY_URL` at its discovery document. + +--- + +## Behind a reverse proxy + +Homelable derives the redirect target from `OIDC_REDIRECT_URI`, so set it to the +**public** HTTPS URL your users reach — not the internal container address. Make +sure the proxy: + +- terminates TLS and forwards `/api/v1/auth/oidc/*` to the backend; +- passes the original `Host` and `X-Forwarded-Proto` headers; +- does not strip the `Cookie` / `Set-Cookie` headers on those routes. + +--- + +## Logout & sessions + +- **Logout** clears the Homelable session cookie (`POST /api/v1/auth/logout`). +- Sessions are stateless JWTs signed with `SECRET_KEY`; there is no server-side + revocation, so a session stays valid until `OIDC_SESSION_EXPIRE_MINUTES` + elapses. Keep the lifetime modest for sensitive deployments. +- Logout is local to Homelable; it does not sign the user out at the IdP. + +--- + +## Troubleshooting + +| Symptom | Likely cause / fix | +|---|---| +| IdP shows `redirect_uri_mismatch` | `OIDC_REDIRECT_URI` must equal the registered URI **exactly**, including scheme, host, port and the `/api/v1/auth/oidc/callback` path. | +| `OIDC authentication failed` (401) after IdP login | Wrong client secret, `openid` missing from scopes, or clock skew breaking `nonce`/`exp` — sync server time (NTP). | +| Logged out immediately / cookie not set | `OIDC_COOKIE_SECURE=true` while serving over HTTP. Serve HTTPS, or set `OIDC_COOKIE_SECURE=false` for local testing only. | +| `403 CSRF validation failed` on save/actions | The request `Origin` is not in `CORS_ORIGINS`, or a stale tab is missing the CSRF token — reload the page. | +| Backend won't start in OIDC mode | Re-read the error: a required `OIDC_*` value is missing, `SECRET_KEY` < 32 bytes, `CORS_ORIGINS` has `*`, or the redirect isn't HTTPS while the cookie is secure. | +| Login button doesn't appear | `AUTH_MODE` is still `local`, or the frontend can't reach `/api/v1/auth/config` — check the proxy and `CORS_ORIGINS`. | + +--- + +## Reverting to local auth + +Set `AUTH_MODE=local` (or remove it) and restart the backend. The +`AUTH_USERNAME` / `AUTH_PASSWORD_HASH` login is restored; the `OIDC_*` values are +ignored while in local mode. From 4d2284dc37edfed41dabb314f5e46420b00034d4 Mon Sep 17 00:00:00 2001 From: ypso Date: Fri, 24 Jul 2026 11:12:57 +0200 Subject: [PATCH 7/8] fix(auth): harden OIDC callback validation --- backend/app/api/routes/auth.py | 3 ++- backend/app/core/config.py | 9 +++++++-- backend/tests/test_auth.py | 12 +++++++++++- backend/tests/test_settings.py | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 2468c37..6f091c6 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -4,6 +4,7 @@ from collections.abc import Mapping from typing import cast from authlib.integrations.base_client.errors import OAuthError +from authlib.jose.errors import JoseError from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from pydantic import BaseModel from starlette.responses import RedirectResponse @@ -86,7 +87,7 @@ async def oidc_callback(request: Request) -> Response: client = get_oidc_client() try: token = await client.authorize_access_token(request) - except OAuthError as exc: + except (OAuthError, JoseError) as exc: logger.warning("OIDC callback rejected: %s", exc.error) request.session.clear() raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="OIDC authentication failed") from None diff --git a/backend/app/core/config.py b/backend/app/core/config.py index ec47043..192137c 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -79,8 +79,13 @@ class Settings(BaseSettings): }.items(): if not value.startswith(("https://", "http://")): raise ValueError(f"{name} must be an HTTP(S) URL") - if self.oidc_cookie_secure and not self.oidc_redirect_uri.startswith("https://"): - raise ValueError("OIDC_REDIRECT_URI must use HTTPS when OIDC_COOKIE_SECURE=true") + if self.oidc_cookie_secure: + for name, value in { + "OIDC_DISCOVERY_URL": self.oidc_discovery_url, + "OIDC_REDIRECT_URI": self.oidc_redirect_uri, + }.items(): + if not value.startswith("https://"): + raise ValueError(f"{name} must use HTTPS when OIDC_COOKIE_SECURE=true") if "*" in self.cors_origins: raise ValueError("CORS_ORIGINS cannot contain '*' when AUTH_MODE=oidc") return self diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 76823b7..d41c1de 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -4,6 +4,7 @@ from urllib.parse import parse_qs, urlparse import pytest from authlib.integrations.base_client.errors import OAuthError +from authlib.jose.errors import ExpiredTokenError, JoseError from fastapi import WebSocketDisconnect from httpx import AsyncClient from starlette.responses import RedirectResponse @@ -11,7 +12,7 @@ from starlette.testclient import TestClient class FakeOIDCClient: - def __init__(self, token=None, error: OAuthError | None = None): + def __init__(self, token=None, error: OAuthError | JoseError | None = None): self.token = token or {} self.error = error self.redirect_uri: str | None = None @@ -290,6 +291,15 @@ async def test_oidc_callback_hides_protocol_error_details(client: AsyncClient, o assert "sensitive detail" not in res.text +async def test_oidc_callback_hides_claim_validation_error_details(client: AsyncClient, oidc_settings): + fake_client = FakeOIDCClient(error=ExpiredTokenError()) + with patch("app.api.routes.auth.get_oidc_client", return_value=fake_client): + res = await client.get("/api/v1/auth/oidc/callback") + assert res.status_code == 401 + assert res.json() == {"detail": "OIDC authentication failed"} + assert "expired" not in res.text.lower() + + async def test_oidc_session_token_cannot_be_used_as_bearer(client: AsyncClient, oidc_settings): from app.core.security import create_oidc_session_token diff --git a/backend/tests/test_settings.py b/backend/tests/test_settings.py index de68e07..8d1d7b0 100644 --- a/backend/tests/test_settings.py +++ b/backend/tests/test_settings.py @@ -203,6 +203,22 @@ def test_oidc_secure_cookie_requires_https_callback(): _valid_oidc_settings(oidc_redirect_uri="http://localhost/api/v1/auth/oidc/callback") +def test_oidc_secure_cookie_requires_https_discovery(): + with pytest.raises(ValueError, match="OIDC_DISCOVERY_URL must use HTTPS"): + _valid_oidc_settings( + oidc_discovery_url="http://idp.local/application/o/homelable/.well-known/openid-configuration" + ) + + +def test_oidc_insecure_dev_mode_allows_http_urls(): + configured = _valid_oidc_settings( + oidc_cookie_secure=False, + oidc_discovery_url="http://idp.local/.well-known/openid-configuration", + oidc_redirect_uri="http://localhost/api/v1/auth/oidc/callback", + ) + assert configured.oidc_cookie_secure is False + + def test_oidc_mode_rejects_wildcard_cors(): with pytest.raises(ValueError, match="CORS_ORIGINS cannot contain"): _valid_oidc_settings(cors_origins=["*"]) From 34758c985071dc9f055de599ba7996aefafa8491 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Fri, 24 Jul 2026 23:59:09 +0200 Subject: [PATCH 8/8] fix(ui): use official Homelable logo on login page The login page rendered lucide's generic Network glyph instead of the brand mark. Use the Logo component there, and realign Logo with the canonical assets in docs/logo: - match icon.svg geometry exactly (bg glow gradient, node glow filter) - fix inverted wordmark colors (Home #e6edf3 / lable #00d4ff) - scope gradient/filter ids per instance so the Sidebar and Toolbar logos don't share defs ids ha-relevant: maybe --- frontend/src/components/LoginPage.tsx | 12 +-- .../components/__tests__/LoginPage.test.tsx | 11 +++ frontend/src/components/ui/Logo.tsx | 76 ++++++++++++++++--- .../src/components/ui/__tests__/Logo.test.tsx | 59 ++++++++++++++ 4 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/ui/__tests__/Logo.test.tsx diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx index bdea9f5..dddcb8e 100644 --- a/frontend/src/components/LoginPage.tsx +++ b/frontend/src/components/LoginPage.tsx @@ -1,8 +1,9 @@ import { useState } from 'react' -import { Network, Loader2 } from 'lucide-react' +import { Loader2 } from 'lucide-react' import { Button, buttonVariants } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Logo } from '@/components/ui/Logo' import { authApi } from '@/api/client' import { useAuthStore } from '@/stores/authStore' import { cn } from '@/lib/utils' @@ -43,11 +44,12 @@ export function LoginPage() {
{/* Logo */}
-
- -
+
-

Homelable

+

+ Home + lable +

HomeLab Visualizer

diff --git a/frontend/src/components/__tests__/LoginPage.test.tsx b/frontend/src/components/__tests__/LoginPage.test.tsx index 7df6e52..72b7f20 100644 --- a/frontend/src/components/__tests__/LoginPage.test.tsx +++ b/frontend/src/components/__tests__/LoginPage.test.tsx @@ -35,6 +35,17 @@ describe('LoginPage', () => { expect(screen.getByRole('button', { name: /sign in/i })).toBeDefined() }) + it('renders the official Homelable logo mark — not a generic lucide icon', () => { + render() + const svg = screen.getByTestId('logo-mark') + // The official mark from docs/logo/icon.svg, not lucide's Network glyph + expect(svg.querySelector('path')?.getAttribute('d')) + .toBe('M32 11 L53 30 L48 30 L48 53 L16 53 L16 30 L11 30 Z') + expect(svg.classList.contains('lucide')).toBe(false) + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Homelable') + expect(screen.getByText('HomeLab Visualizer')).toBeDefined() + }) + it('renders only the provider redirect in OIDC mode', () => { useAuthStore.setState({ authMode: 'oidc', diff --git a/frontend/src/components/ui/Logo.tsx b/frontend/src/components/ui/Logo.tsx index 85a227b..66320bd 100644 --- a/frontend/src/components/ui/Logo.tsx +++ b/frontend/src/components/ui/Logo.tsx @@ -1,10 +1,21 @@ +import { useId } from 'react'; + interface LogoProps { size?: number; showText?: boolean; className?: string; } +/** + * Official Homelable mark — kept in sync with `docs/logo/icon.svg`. + * Wordmark colors follow `docs/logo/logo-md.svg` (Home = #e6edf3, lable = #00d4ff). + */ export function Logo({ size = 32, showText = true, className = '' }: LogoProps) { + // Colons are legal in a URL fragment but confuse some tooling — strip them. + const uid = useId().replace(/:/g, ''); + const bgGlow = `homelable-bg-glow-${uid}`; + const nodeGlow = `homelable-node-glow-${uid}`; + return (
{showText && ( - Home - lable + Home + lable )}
diff --git a/frontend/src/components/ui/__tests__/Logo.test.tsx b/frontend/src/components/ui/__tests__/Logo.test.tsx new file mode 100644 index 0000000..17b2863 --- /dev/null +++ b/frontend/src/components/ui/__tests__/Logo.test.tsx @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import { Logo } from '../Logo' + +describe('Logo', () => { + it('renders the official mark (house + network nodes), not a placeholder icon', () => { + render() + const svg = screen.getByTestId('logo-mark') + // Canonical house outline from docs/logo/icon.svg + expect(svg.querySelector('path')?.getAttribute('d')) + .toBe('M32 11 L53 30 L48 30 L48 53 L16 53 L16 30 L11 30 Z') + // Center hub + 3 peripheral nodes + hub glow + 2 background circles + expect(svg.querySelectorAll('circle').length).toBe(7) + }) + + it('uses the canonical wordmark colors (Home #e6edf3 / lable #00d4ff)', () => { + render() + expect(screen.getByText('Home')).toHaveStyle({ color: '#e6edf3' }) + expect(screen.getByText('lable')).toHaveStyle({ color: '#00d4ff' }) + }) + + it('hides the wordmark when showText is false', () => { + render() + expect(screen.queryByText('Home')).toBeNull() + expect(screen.getByTestId('logo-mark')).toBeInTheDocument() + }) + + it('scales the svg to the requested size', () => { + render() + const svg = screen.getByTestId('logo-mark') + expect(svg.getAttribute('width')).toBe('64') + expect(svg.getAttribute('height')).toBe('64') + }) + + it('generates unique gradient/filter ids per instance — two logos can coexist', () => { + // Sidebar and Toolbar both render a Logo at the same time; duplicate SVG + // defs ids would make one instance reference the other's gradient. + render(<>) + const [first, second] = screen.getAllByTestId('logo-mark') + const idOf = (svg: HTMLElement) => svg.querySelector('radialGradient')!.getAttribute('id') + expect(idOf(first)).not.toBe(idOf(second)) + + // Each instance must point at its own gradient + const gradientRef = (svg: HTMLElement) => + svg.querySelectorAll('circle')[1].getAttribute('fill') + expect(gradientRef(first)).toBe(`url(#${idOf(first)!})`) + expect(gradientRef(second)).toBe(`url(#${idOf(second)!})`) + }) + + it('marks the svg aria-hidden — the adjacent wordmark carries the name', () => { + render() + expect(screen.getByTestId('logo-mark')).toHaveAttribute('aria-hidden', 'true') + }) + + it('forwards className to the wrapper', () => { + const { container } = render() + expect(container.firstElementChild).toHaveClass('my-class') + }) +})