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/.github/workflows/security.yml b/.github/workflows/security.yml index 3118aec..7c056eb 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -31,11 +31,12 @@ 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' - 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 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/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..6f091c6 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -1,12 +1,27 @@ 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 authlib.jose.errors import JoseError +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 +34,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 +72,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, 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 + + 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..192137c 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,35 @@ 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: + 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 # 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..d41c1de 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1,5 +1,56 @@ +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 authlib.jose.errors import ExpiredTokenError, JoseError +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 | JoseError | 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 +61,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 +133,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 +154,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 +193,231 @@ 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_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 + + 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..8d1d7b0 100644 --- a/backend/tests/test_settings.py +++ b/backend/tests/test_settings.py @@ -167,3 +167,64 @@ 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_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=["*"]) + + +def test_valid_oidc_configuration_is_accepted(): + configured = _valid_oidc_settings() + assert configured.auth_mode == "oidc" + assert configured.oidc_cookie_secure is True 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. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f31c319..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": { @@ -2520,9 +2585,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2537,9 +2599,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2554,9 +2613,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2571,9 +2627,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2588,9 +2641,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2605,9 +2655,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2622,9 +2669,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2639,9 +2683,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2656,9 +2697,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2673,9 +2711,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2690,9 +2725,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2707,9 +2739,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2724,9 +2753,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2821,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" @@ -2975,9 +3003,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2995,9 +3020,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3015,9 +3037,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3035,9 +3054,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3077,6 +3093,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", @@ -3220,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", @@ -3231,15 +3312,17 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@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==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3252,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" @@ -3414,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" @@ -3444,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": { @@ -3651,9 +3737,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": { @@ -3976,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", @@ -3989,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" @@ -3998,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" @@ -4037,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" @@ -4063,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" @@ -4080,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", @@ -4096,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" @@ -4111,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" @@ -4152,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" @@ -4186,16 +4282,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", @@ -4207,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" @@ -4229,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", @@ -4250,9 +4374,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": { @@ -4264,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" @@ -4276,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", @@ -4309,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" @@ -4324,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" @@ -4346,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", @@ -4362,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" @@ -4371,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", @@ -4436,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" @@ -4451,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" @@ -4463,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" @@ -4472,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", @@ -4486,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", @@ -4506,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" @@ -4518,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", @@ -4544,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" @@ -4562,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": { @@ -4580,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" @@ -4596,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" @@ -4609,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" @@ -4618,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" @@ -4633,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" @@ -4642,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", @@ -4659,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", @@ -4685,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", @@ -4720,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" @@ -4880,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" @@ -4927,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" @@ -4948,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" @@ -4957,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", @@ -4973,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" @@ -4985,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" @@ -5006,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" @@ -5035,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" @@ -5052,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" @@ -5078,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", @@ -5095,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" @@ -5149,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" @@ -5158,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" @@ -5261,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" @@ -5270,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": { @@ -5427,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", @@ -5496,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" @@ -5505,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" @@ -5517,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" @@ -5526,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", @@ -5562,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", @@ -5605,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" @@ -5623,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" @@ -5632,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" @@ -5648,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", @@ -5670,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" @@ -5696,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", @@ -5712,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" @@ -5721,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" @@ -5738,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", @@ -5761,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" @@ -5789,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" @@ -5801,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", @@ -5896,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" @@ -5908,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" @@ -5917,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" @@ -5926,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", @@ -5964,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" @@ -5979,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.*" @@ -5988,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" @@ -6024,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" @@ -6049,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", @@ -6103,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": { @@ -6118,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" @@ -6176,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": { @@ -6199,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" @@ -6234,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", @@ -6268,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", @@ -6281,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" @@ -6290,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" @@ -6306,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" @@ -6315,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", @@ -6351,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" @@ -6366,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" @@ -6375,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" @@ -6396,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" @@ -6405,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" @@ -6414,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" @@ -6426,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" @@ -6438,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" @@ -6456,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" @@ -6468,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" @@ -6483,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" @@ -6495,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" @@ -6514,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" @@ -6532,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" @@ -6544,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" @@ -6556,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" @@ -6571,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": { @@ -6626,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" @@ -6635,12 +6866,13 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -6704,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" @@ -6723,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": { @@ -6736,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": { @@ -6749,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" @@ -6761,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" @@ -6783,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" @@ -6945,9 +7183,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6969,9 +7204,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6993,9 +7225,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7017,9 +7246,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7079,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": { @@ -7114,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", @@ -7130,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" @@ -7142,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" @@ -7154,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" @@ -7251,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" @@ -7260,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" @@ -7272,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" @@ -7287,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", @@ -7300,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" @@ -7333,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" @@ -7342,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" @@ -7377,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" @@ -7392,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": { @@ -7436,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" @@ -7449,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" @@ -7458,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", @@ -7483,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" @@ -7503,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", @@ -7522,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", @@ -7540,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", @@ -7562,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" @@ -7574,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" @@ -7583,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" @@ -7595,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" @@ -7615,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" @@ -7627,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" @@ -7636,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" @@ -7651,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", @@ -7689,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", @@ -7712,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" @@ -7724,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": { @@ -7762,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" @@ -7774,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", @@ -7792,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" @@ -7817,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" @@ -7826,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": { @@ -7842,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" @@ -7851,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": { @@ -7864,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" @@ -7882,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" @@ -7891,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", @@ -7919,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", @@ -7932,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" @@ -7984,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" @@ -7999,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", @@ -8012,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" @@ -8021,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", @@ -8053,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" @@ -8068,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", @@ -8088,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" @@ -8097,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", @@ -8151,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", @@ -8181,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" @@ -8190,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" @@ -8205,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" @@ -8214,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", @@ -8230,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", @@ -8291,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", @@ -8307,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", @@ -8317,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" @@ -8329,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", @@ -8352,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": { @@ -8377,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" @@ -8386,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", @@ -8412,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" @@ -8421,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" @@ -8437,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", @@ -8456,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", @@ -8507,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" @@ -8516,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" @@ -8528,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" @@ -8537,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", @@ -8556,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", @@ -8572,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", @@ -8590,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", @@ -8616,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" @@ -8628,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": { @@ -8644,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" @@ -8653,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" @@ -8669,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" @@ -8685,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" @@ -8697,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", @@ -8720,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", @@ -8737,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" @@ -8752,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" @@ -8764,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" @@ -8773,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" @@ -8837,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" @@ -8880,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": { @@ -8930,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" @@ -8942,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" @@ -8960,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" @@ -8969,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" @@ -9007,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", @@ -9017,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", @@ -9031,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": { @@ -9059,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" @@ -9074,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", @@ -9088,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" @@ -9097,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" @@ -9113,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", @@ -9161,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" @@ -9180,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" @@ -9189,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" @@ -9198,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" @@ -9207,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", @@ -9256,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" @@ -9271,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" @@ -9450,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" @@ -9494,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" @@ -9536,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", @@ -9550,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", @@ -9570,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" @@ -9582,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", @@ -9621,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" @@ -9630,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", @@ -9654,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" @@ -9663,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", @@ -9683,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" @@ -9708,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" @@ -9723,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" @@ -9735,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" @@ -9747,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" @@ -9756,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", 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..dddcb8e 100644 --- a/frontend/src/components/LoginPage.tsx +++ b/frontend/src/components/LoginPage.tsx @@ -1,17 +1,19 @@ import { useState } from 'react' -import { Network, Loader2 } from 'lucide-react' -import { Button } from '@/components/ui/button' +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' 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() @@ -42,62 +44,92 @@ export function LoginPage() {
{/* Logo */}
-
- -
+
-

Homelable

+

+ Home + lable +

HomeLab Visualizer

- {/* 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..72b7f20 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,41 @@ 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', + 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 +142,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/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') + }) +}) 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, + } + }, } ) )