diff --git a/apps/api/alembic.ini b/apps/api/alembic.ini
new file mode 100644
index 0000000..fc3f49f
--- /dev/null
+++ b/apps/api/alembic.ini
@@ -0,0 +1,36 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = postgresql+asyncpg://headquarter:headquarter@postgres:5432/headquarter
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
diff --git a/apps/api/alembic/env.py b/apps/api/alembic/env.py
new file mode 100644
index 0000000..87a4eff
--- /dev/null
+++ b/apps/api/alembic/env.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import pool
+from sqlalchemy.engine import Connection
+from sqlalchemy.ext.asyncio import async_engine_from_config
+
+from src.config import Settings
+from src.models import Base
+
+config = context.config
+
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+settings = Settings()
+config.set_main_option("sqlalchemy.url", settings.database_url)
+
+target_metadata = Base.metadata
+
+
+def run_migrations_offline() -> None:
+ context.configure(
+ url=settings.database_url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def do_run_migrations(connection: Connection) -> None:
+ context.configure(connection=connection, target_metadata=target_metadata)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+async def run_async_migrations() -> None:
+ connectable = async_engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ async with connectable.connect() as connection:
+ await connection.run_sync(do_run_migrations)
+
+ await connectable.dispose()
+
+
+def run_migrations_online() -> None:
+ import asyncio
+
+ asyncio.run(run_async_migrations())
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/apps/api/alembic/script.py.mako b/apps/api/alembic/script.py.mako
new file mode 100644
index 0000000..04fd074
--- /dev/null
+++ b/apps/api/alembic/script.py.mako
@@ -0,0 +1,25 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+"""
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/apps/api/alembic/versions/0001_initial_schema.py b/apps/api/alembic/versions/0001_initial_schema.py
new file mode 100644
index 0000000..7595a37
--- /dev/null
+++ b/apps/api/alembic/versions/0001_initial_schema.py
@@ -0,0 +1,106 @@
+"""initial schema
+
+Revision ID: 0001_initial_schema
+Revises:
+Create Date: 2026-05-17 00:00:00.000000
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+revision = "0001_initial_schema"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+TABLE_NAMES = [
+ "users",
+ "ssh_keys",
+ "projects",
+ "git_repositories",
+ "user_configs",
+]
+
+
+def upgrade() -> None:
+ op.create_table(
+ "users",
+ sa.Column("email", sa.String(length=255), nullable=False),
+ sa.Column("name", sa.String(length=255), nullable=False),
+ sa.Column("authentik_id", sa.String(length=255), nullable=False),
+ sa.Column("avatar_url", sa.String(length=1024), nullable=True),
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("authentik_id"),
+ sa.UniqueConstraint("email"),
+ )
+ op.create_index(op.f("ix_users_authentik_id"), "users", ["authentik_id"], unique=True)
+ op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
+
+ op.create_table(
+ "ssh_keys",
+ sa.Column("name", sa.String(length=255), nullable=False),
+ sa.Column("public_key", sa.Text(), nullable=False),
+ sa.Column("private_key_encrypted", sa.Text(), nullable=False),
+ sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+ )
+
+ op.create_table(
+ "projects",
+ sa.Column("name", sa.String(length=255), nullable=False),
+ sa.Column("description", sa.Text(), nullable=True),
+ sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("default_ssh_key_id", postgresql.UUID(as_uuid=True), nullable=True),
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ sa.ForeignKeyConstraint(["default_ssh_key_id"], ["ssh_keys.id"]),
+ sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
+ )
+
+ op.create_table(
+ "git_repositories",
+ sa.Column("name", sa.String(length=255), nullable=False),
+ sa.Column("path", sa.String(length=1024), nullable=False),
+ sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("is_mirror", sa.Boolean(), nullable=False),
+ sa.Column("remote_url", sa.String(length=1024), nullable=True),
+ sa.Column("last_push", sa.DateTime(timezone=True), nullable=True),
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
+ sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
+ )
+
+ op.create_table(
+ "user_configs",
+ sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("user_id"),
+ sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+ )
+
+
+def downgrade() -> None:
+ op.drop_table("user_configs")
+ op.drop_table("git_repositories")
+ op.drop_table("projects")
+ op.drop_table("ssh_keys")
+ op.drop_index(op.f("ix_users_email"), table_name="users")
+ op.drop_index(op.f("ix_users_authentik_id"), table_name="users")
+ op.drop_table("users")
diff --git a/apps/api/alembic/versions/0002_refresh_tokens.py b/apps/api/alembic/versions/0002_refresh_tokens.py
new file mode 100644
index 0000000..c206a43
--- /dev/null
+++ b/apps/api/alembic/versions/0002_refresh_tokens.py
@@ -0,0 +1,58 @@
+"""add refresh tokens table
+
+Revision ID: 0002_refresh_tokens
+Revises: 0001_initial_schema
+Create Date: 2026-05-17 00:00:01.000000
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+revision = "0002_refresh_tokens"
+down_revision = "0001_initial_schema"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ connection = op.get_bind()
+ inspector = sa.inspect(connection)
+
+ if not inspector.has_table("refresh_tokens"):
+ op.create_table(
+ "refresh_tokens",
+ sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("token_hash", sa.String(length=255), nullable=False),
+ sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
+ sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
+ sa.Column("user_agent", sa.String(length=512), nullable=True),
+ sa.Column("ip_address", sa.String(length=64), nullable=True),
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("token_hash"),
+ )
+
+ existing_indexes = {index["name"] for index in inspector.get_indexes("refresh_tokens")}
+ user_index = op.f("ix_refresh_tokens_user_id")
+ expires_index = op.f("ix_refresh_tokens_expires_at")
+ if user_index not in existing_indexes:
+ op.create_index(user_index, "refresh_tokens", ["user_id"], unique=False)
+ if expires_index not in existing_indexes:
+ op.create_index(expires_index, "refresh_tokens", ["expires_at"], unique=False)
+
+
+def downgrade() -> None:
+ connection = op.get_bind()
+ inspector = sa.inspect(connection)
+ if inspector.has_table("refresh_tokens"):
+ existing_indexes = {index["name"] for index in inspector.get_indexes("refresh_tokens")}
+ expires_index = op.f("ix_refresh_tokens_expires_at")
+ user_index = op.f("ix_refresh_tokens_user_id")
+ if expires_index in existing_indexes:
+ op.drop_index(expires_index, table_name="refresh_tokens")
+ if user_index in existing_indexes:
+ op.drop_index(user_index, table_name="refresh_tokens")
+ op.drop_table("refresh_tokens")
diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml
index e4c8330..a8dd110 100644
--- a/apps/api/pyproject.toml
+++ b/apps/api/pyproject.toml
@@ -26,3 +26,6 @@ dev = [
"ruff>=0.1.0",
"httpx>=0.25.0",
]
+
+[tool.pytest.ini_options]
+pythonpath = ["."]
diff --git a/apps/api/src/__init__.py b/apps/api/src/__init__.py
new file mode 100644
index 0000000..0c3a5d3
--- /dev/null
+++ b/apps/api/src/__init__.py
@@ -0,0 +1 @@
+"""Headquarter API package."""
diff --git a/apps/api/src/api/__init__.py b/apps/api/src/api/__init__.py
new file mode 100644
index 0000000..a2b342e
--- /dev/null
+++ b/apps/api/src/api/__init__.py
@@ -0,0 +1,3 @@
+from src.api.auth import router as auth_router
+
+__all__ = ["auth_router"]
diff --git a/apps/api/src/api/auth.py b/apps/api/src/api/auth.py
new file mode 100644
index 0000000..bfbbd5b
--- /dev/null
+++ b/apps/api/src/api/auth.py
@@ -0,0 +1,190 @@
+from secrets import token_urlsafe
+from datetime import UTC, datetime, timedelta
+from typing import AsyncGenerator, Literal, cast
+
+import httpx
+from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
+from fastapi.responses import RedirectResponse
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src.auth.cookies import build_cookie_options
+from src.auth.jwt_service import decode_access_token, mint_access_token
+from src.auth.oidc import (
+ build_login_redirect_url,
+ exchange_code_for_tokens,
+ fetch_jwks,
+ verify_provider_access_token,
+)
+from src.auth.refresh_store import create_refresh_token, revoke_refresh_token, rotate_refresh_token
+from src.config import Settings
+from src.database import SessionLocal
+from src.models.user import User
+
+router = APIRouter(prefix="/auth", tags=["auth"])
+
+
+async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
+ async with SessionLocal() as session:
+ yield session
+
+
+@router.get("/login")
+async def login() -> RedirectResponse:
+ settings = Settings()
+ redirect_uri = "http://localhost:8000/auth/callback"
+ state = token_urlsafe(24)
+ location = build_login_redirect_url(
+ settings=settings,
+ redirect_uri=redirect_uri,
+ state=state,
+ nonce=token_urlsafe(16),
+ )
+ response = RedirectResponse(location)
+ response.set_cookie("auth_state", state, httponly=True, samesite="lax")
+ return response
+
+
+@router.get("/callback")
+async def callback(
+ code: str,
+ state: str,
+ response: Response,
+ auth_state: str | None = Cookie(default=None),
+ session: AsyncSession = Depends(get_db_session),
+) -> dict[str, str]:
+ if auth_state is None or auth_state != state:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid state")
+
+ settings = Settings()
+ redirect_uri = "http://localhost:8000/auth/callback"
+ async with httpx.AsyncClient() as client:
+ token_payload = await exchange_code_for_tokens(
+ settings=settings,
+ code=code,
+ redirect_uri=redirect_uri,
+ client=client,
+ )
+ jwks = await fetch_jwks(settings=settings, client=client)
+
+ provider_claims = verify_provider_access_token(
+ settings=settings,
+ token=token_payload["access_token"],
+ jwks=jwks,
+ )
+
+ authentik_id = str(provider_claims["sub"])
+ email = str(provider_claims.get("email", f"{authentik_id}@authentik.local"))
+ name = str(provider_claims.get("name", email))
+
+ user = await session.scalar(select(User).where(User.authentik_id == authentik_id))
+ if user is None:
+ user = User(email=email, name=name, authentik_id=authentik_id, avatar_url=None)
+ session.add(user)
+ await session.commit()
+ await session.refresh(user)
+ else:
+ user.email = email
+ user.name = name
+ await session.commit()
+
+ access_token = mint_access_token(
+ settings=settings,
+ subject=str(user.id),
+ email=user.email,
+ name=user.name,
+ expires_at=datetime.now(UTC) + timedelta(minutes=settings.access_token_ttl_minutes),
+ )
+ refresh_token, _ = await create_refresh_token(
+ session=session,
+ user_id=user.id,
+ expires_at=datetime.now(UTC) + timedelta(days=settings.refresh_token_ttl_days),
+ user_agent=None,
+ ip_address=None,
+ )
+
+ cookie_options = build_cookie_options(settings)
+ cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
+ cookie_secure = bool(cookie_options["secure"])
+ response.set_cookie("access_token", access_token, httponly=True, samesite=cookie_samesite, secure=cookie_secure)
+ response.set_cookie("refresh_token", refresh_token, httponly=True, samesite=cookie_samesite, secure=cookie_secure)
+ response.delete_cookie("auth_state", samesite="lax")
+
+ return {"sub": str(user.id), "email": user.email, "name": user.name}
+
+
+@router.post("/refresh")
+async def refresh(
+ response: Response,
+ refresh_token: str | None = Cookie(default=None),
+ session: AsyncSession = Depends(get_db_session),
+) -> dict[str, str]:
+ if not refresh_token:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing refresh token")
+
+ settings = Settings()
+ try:
+ rotated_raw_token, rotated_record = await rotate_refresh_token(
+ session=session,
+ raw_token=refresh_token,
+ user_agent=None,
+ ip_address=None,
+ )
+ except ValueError as error:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(error)) from error
+
+ user = await session.get(User, rotated_record.user_id)
+ if user is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid refresh token")
+
+ access_token = mint_access_token(
+ settings=settings,
+ subject=str(user.id),
+ email=user.email,
+ name=user.name,
+ expires_at=datetime.now(UTC) + timedelta(minutes=settings.access_token_ttl_minutes),
+ )
+
+ cookie_options = build_cookie_options(settings)
+ cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
+ cookie_secure = bool(cookie_options["secure"])
+
+ response.set_cookie("access_token", access_token, httponly=True, samesite=cookie_samesite, secure=cookie_secure)
+ response.set_cookie("refresh_token", rotated_raw_token, httponly=True, samesite=cookie_samesite, secure=cookie_secure)
+
+ return {"sub": str(user.id), "email": user.email, "name": user.name}
+
+
+@router.post("/logout")
+async def logout(
+ response: Response,
+ refresh_token: str | None = Cookie(default=None),
+ session: AsyncSession = Depends(get_db_session),
+) -> dict[str, str]:
+ settings = Settings()
+ cookie_options = build_cookie_options(settings)
+ cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
+ cookie_secure = bool(cookie_options["secure"])
+
+ if refresh_token:
+ try:
+ await revoke_refresh_token(session=session, raw_token=refresh_token)
+ except Exception:
+ pass
+
+ response.delete_cookie("access_token", samesite=cookie_samesite, secure=cookie_secure)
+ response.delete_cookie("refresh_token", samesite=cookie_samesite, secure=cookie_secure)
+ return {"status": "ok"}
+
+
+@router.get("/me")
+async def me(access_token: str | None = Cookie(default=None)) -> dict[str, str]:
+ if not access_token:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing access token")
+
+ claims = decode_access_token(settings=Settings(), token=access_token)
+ return {
+ "sub": str(claims["sub"]),
+ "email": str(claims["email"]),
+ "name": str(claims["name"]),
+ }
diff --git a/apps/api/src/api/projects.py b/apps/api/src/api/projects.py
new file mode 100644
index 0000000..be8e5c3
--- /dev/null
+++ b/apps/api/src/api/projects.py
@@ -0,0 +1,163 @@
+import uuid
+from typing import Annotated
+
+from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
+from pydantic import BaseModel, ConfigDict
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src.auth.jwt_service import decode_access_token
+from src.config import Settings
+from src.database import SessionLocal
+from src.models.project import Project
+from src.models.ssh_key import SSHKey
+from src.models.user import User
+
+router = APIRouter(prefix="/projects", tags=["projects"])
+
+
+async def get_db_session():
+ async with SessionLocal() as session:
+ yield session
+
+
+async def get_current_user_id(
+ access_token: Annotated[str | None, Cookie()] = None,
+) -> uuid.UUID:
+ if not access_token:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing access token")
+
+ try:
+ claims = decode_access_token(settings=Settings(), token=access_token)
+ return uuid.UUID(str(claims["sub"]))
+ except Exception:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid access token")
+
+
+async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
+ user = await session.get(User, user_id)
+ if user is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found")
+ return user
+
+
+class ProjectCreate(BaseModel):
+ name: str
+ description: str | None = None
+
+
+class ProjectUpdate(BaseModel):
+ name: str | None = None
+ description: str | None = None
+
+
+class ProjectResponse(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: uuid.UUID
+ name: str
+ description: str | None
+ owner_id: uuid.UUID
+ default_ssh_key_id: uuid.UUID | None
+
+
+class SetDefaultSSHKeyRequest(BaseModel):
+ ssh_key_id: uuid.UUID
+
+
+@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
+async def create_project(
+ data: ProjectCreate,
+ user_id: uuid.UUID = Depends(get_current_user_id),
+ session: AsyncSession = Depends(get_db_session),
+) -> Project:
+ user = await _get_user(session, user_id)
+ project = Project(
+ name=data.name,
+ description=data.description,
+ owner_id=user.id,
+ default_ssh_key_id=None,
+ )
+ session.add(project)
+ await session.commit()
+ await session.refresh(project)
+ return project
+
+
+@router.get("", response_model=list[ProjectResponse])
+async def list_projects(
+ user_id: uuid.UUID = Depends(get_current_user_id),
+ session: AsyncSession = Depends(get_db_session),
+) -> list[Project]:
+ user = await _get_user(session, user_id)
+ result = await session.execute(select(Project).where(Project.owner_id == user.id))
+ return list(result.scalars().all())
+
+
+async def _get_owned_project(
+ project_id: uuid.UUID,
+ user_id: uuid.UUID,
+ session: AsyncSession,
+) -> Project:
+ project = await session.get(Project, project_id)
+ if project is None:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="project not found")
+ if project.owner_id != user_id:
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not project owner")
+ return project
+
+
+@router.patch("/{project_id}", response_model=ProjectResponse)
+async def update_project(
+ project_id: uuid.UUID,
+ data: ProjectUpdate,
+ user_id: uuid.UUID = Depends(get_current_user_id),
+ session: AsyncSession = Depends(get_db_session),
+) -> Project:
+ await _get_user(session, user_id)
+ project = await _get_owned_project(project_id, user_id, session)
+
+ if data.name is not None:
+ project.name = data.name
+ if data.description is not None:
+ project.description = data.description
+
+ await session.commit()
+ await session.refresh(project)
+ return project
+
+
+@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_project(
+ project_id: uuid.UUID,
+ user_id: uuid.UUID = Depends(get_current_user_id),
+ session: AsyncSession = Depends(get_db_session),
+) -> Response:
+ await _get_user(session, user_id)
+ project = await _get_owned_project(project_id, user_id, session)
+ await session.delete(project)
+ await session.commit()
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
+
+
+@router.patch("/{project_id}/default-ssh-key", response_model=ProjectResponse)
+async def set_default_ssh_key(
+ project_id: uuid.UUID,
+ data: SetDefaultSSHKeyRequest,
+ user_id: uuid.UUID = Depends(get_current_user_id),
+ session: AsyncSession = Depends(get_db_session),
+) -> Project:
+ user = await _get_user(session, user_id)
+ project = await _get_owned_project(project_id, user_id, session)
+
+ ssh_key = await session.get(SSHKey, data.ssh_key_id)
+ if ssh_key is None or ssh_key.user_id != user.id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="invalid ssh key",
+ )
+
+ project.default_ssh_key_id = data.ssh_key_id
+ await session.commit()
+ await session.refresh(project)
+ return project
diff --git a/apps/api/src/auth/__init__.py b/apps/api/src/auth/__init__.py
new file mode 100644
index 0000000..1611ea7
--- /dev/null
+++ b/apps/api/src/auth/__init__.py
@@ -0,0 +1,12 @@
+from src.auth.cookies import build_cookie_options
+from src.auth.jwt_service import decode_access_token, mint_access_token
+from src.auth.oidc import build_login_redirect_url
+from src.auth.refresh_store import hash_refresh_token
+
+__all__ = [
+ "build_cookie_options",
+ "build_login_redirect_url",
+ "decode_access_token",
+ "hash_refresh_token",
+ "mint_access_token",
+]
diff --git a/apps/api/src/auth/cookies.py b/apps/api/src/auth/cookies.py
new file mode 100644
index 0000000..0a72f98
--- /dev/null
+++ b/apps/api/src/auth/cookies.py
@@ -0,0 +1,9 @@
+from src.config import Settings
+
+
+def build_cookie_options(settings: Settings) -> dict[str, str | bool]:
+ return {
+ "httponly": True,
+ "secure": settings.cookie_secure,
+ "samesite": settings.cookie_samesite,
+ }
diff --git a/apps/api/src/auth/jwt_service.py b/apps/api/src/auth/jwt_service.py
new file mode 100644
index 0000000..8549d5e
--- /dev/null
+++ b/apps/api/src/auth/jwt_service.py
@@ -0,0 +1,27 @@
+from datetime import datetime
+
+from jose import jwt # type: ignore[import-untyped]
+
+from src.config import Settings
+
+
+def mint_access_token(
+ *,
+ settings: Settings,
+ subject: str,
+ email: str,
+ name: str,
+ expires_at: datetime,
+) -> str:
+ payload = {
+ "sub": subject,
+ "email": email,
+ "name": name,
+ "exp": expires_at,
+ }
+ return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
+
+
+def decode_access_token(*, settings: Settings, token: str) -> dict[str, str | int]:
+ claims = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm])
+ return dict(claims)
diff --git a/apps/api/src/auth/oidc.py b/apps/api/src/auth/oidc.py
new file mode 100644
index 0000000..23ccecd
--- /dev/null
+++ b/apps/api/src/auth/oidc.py
@@ -0,0 +1,77 @@
+from urllib.parse import urlencode
+
+import httpx
+from jose import jwt # type: ignore[import-untyped]
+
+from src.config import Settings
+
+
+def build_login_redirect_url(
+ *,
+ settings: Settings,
+ redirect_uri: str,
+ state: str,
+ nonce: str,
+) -> str:
+ query = urlencode(
+ {
+ "response_type": "code",
+ "client_id": settings.authentik_client_id,
+ "redirect_uri": redirect_uri,
+ "scope": "openid profile email",
+ "state": state,
+ "nonce": nonce,
+ }
+ )
+ return f"{settings.authentik_authorize_url}?{query}"
+
+
+async def exchange_code_for_tokens(
+ *,
+ settings: Settings,
+ code: str,
+ redirect_uri: str,
+ client: httpx.AsyncClient,
+) -> dict[str, str]:
+ response = await client.post(
+ settings.authentik_token_url,
+ data={
+ "grant_type": "authorization_code",
+ "code": code,
+ "redirect_uri": redirect_uri,
+ "client_id": settings.authentik_client_id,
+ "client_secret": settings.authentik_client_secret,
+ },
+ )
+ response.raise_for_status()
+ payload = response.json()
+ return {
+ "access_token": payload["access_token"],
+ "refresh_token": payload["refresh_token"],
+ }
+
+
+async def fetch_jwks(*, settings: Settings, client: httpx.AsyncClient) -> dict[str, list[dict[str, str]]]:
+ response = await client.get(settings.authentik_jwks_url)
+ response.raise_for_status()
+ payload = response.json()
+ return {"keys": payload["keys"]}
+
+
+def verify_provider_access_token(
+ *,
+ settings: Settings,
+ token: str,
+ jwks: dict[str, list[dict[str, str]]],
+) -> dict[str, str | int]:
+ unverified_header = jwt.get_unverified_header(token)
+ key_id = unverified_header["kid"]
+ jwk_key = next(key for key in jwks["keys"] if key.get("kid") == key_id)
+ claims = jwt.decode(
+ token,
+ jwk_key,
+ algorithms=[jwk_key.get("alg", "HS256")],
+ audience=settings.authentik_audience,
+ issuer=settings.authentik_issuer,
+ )
+ return dict(claims)
diff --git a/apps/api/src/auth/refresh_store.py b/apps/api/src/auth/refresh_store.py
new file mode 100644
index 0000000..bad8adc
--- /dev/null
+++ b/apps/api/src/auth/refresh_store.py
@@ -0,0 +1,79 @@
+from datetime import UTC, datetime
+from hashlib import sha256
+from secrets import token_urlsafe
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src.models.refresh_token import RefreshToken
+
+
+def hash_refresh_token(raw_token: str) -> str:
+ return sha256(raw_token.encode("utf-8")).hexdigest()
+
+
+async def create_refresh_token(
+ *,
+ session: AsyncSession,
+ user_id: object,
+ expires_at: datetime,
+ user_agent: str | None,
+ ip_address: str | None,
+) -> tuple[str, RefreshToken]:
+ raw_token = token_urlsafe(48)
+ record = RefreshToken(
+ user_id=user_id,
+ token_hash=hash_refresh_token(raw_token),
+ expires_at=expires_at,
+ created_at=datetime.now(UTC),
+ user_agent=user_agent,
+ ip_address=ip_address,
+ )
+ session.add(record)
+ await session.commit()
+ await session.refresh(record)
+ return raw_token, record
+
+
+async def rotate_refresh_token(
+ *,
+ session: AsyncSession,
+ raw_token: str,
+ user_agent: str | None,
+ ip_address: str | None,
+) -> tuple[str, RefreshToken]:
+ existing_hash = hash_refresh_token(raw_token)
+ existing = await session.scalar(
+ select(RefreshToken).where(
+ RefreshToken.token_hash == existing_hash,
+ RefreshToken.revoked_at.is_(None),
+ )
+ )
+ if existing is None:
+ raise ValueError("refresh token not found")
+ if existing.expires_at <= datetime.now(UTC):
+ raise ValueError("refresh token expired")
+
+ existing.revoked_at = datetime.now(UTC)
+ await session.flush()
+
+ return await create_refresh_token(
+ session=session,
+ user_id=existing.user_id,
+ expires_at=existing.expires_at,
+ user_agent=user_agent,
+ ip_address=ip_address,
+ )
+
+
+async def revoke_refresh_token(*, session: AsyncSession, raw_token: str) -> bool:
+ token_hash = hash_refresh_token(raw_token)
+ existing = await session.scalar(select(RefreshToken).where(RefreshToken.token_hash == token_hash))
+ if existing is None:
+ return False
+ if existing.revoked_at is not None:
+ return True
+
+ existing.revoked_at = datetime.now(UTC)
+ await session.commit()
+ return True
diff --git a/apps/api/src/config.py b/apps/api/src/config.py
new file mode 100644
index 0000000..548b42a
--- /dev/null
+++ b/apps/api/src/config.py
@@ -0,0 +1,62 @@
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+def build_database_url(
+ *,
+ user: str,
+ password: str,
+ host: str,
+ port: int,
+ database: str,
+) -> str:
+ return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{database}"
+
+
+class Settings(BaseSettings):
+ app_env: str = "development"
+ database_url_override: str | None = Field(default=None, alias="DATABASE_URL")
+ postgres_user: str = "headquarter"
+ postgres_password: str = "headquarter"
+ postgres_host: str = "postgres"
+ postgres_port: int = 5432
+ postgres_db: str = "headquarter"
+
+ authentik_client_id: str = "headquarter-web"
+ authentik_client_secret: str = "change-me"
+ authentik_authorize_url: str = "https://authentik.local/application/o/authorize/"
+ authentik_token_url: str = "https://authentik.local/application/o/token/"
+ authentik_jwks_url: str = "https://authentik.local/application/o/headquarter-web/jwks/"
+ authentik_issuer: str = "https://authentik.local/application/o/headquarter-web/"
+ authentik_audience: str = "headquarter-web"
+
+ jwt_secret: str = "change-me-jwt-secret"
+ jwt_algorithm: str = "HS256"
+ access_token_ttl_minutes: int = 15
+ refresh_token_ttl_days: int = 7
+
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore", populate_by_name=True)
+
+ @property
+ def database_url(self) -> str:
+ if self.database_url_override:
+ return self.database_url_override
+
+ return build_database_url(
+ user=self.postgres_user,
+ password=self.postgres_password,
+ host=self.postgres_host,
+ port=self.postgres_port,
+ database=self.postgres_db,
+ )
+
+ @property
+ def cookie_secure(self) -> bool:
+ return self.app_env == "production"
+
+ @property
+ def cookie_samesite(self) -> str:
+ if self.app_env == "production":
+ return "strict"
+
+ return "lax"
diff --git a/apps/api/src/database.py b/apps/api/src/database.py
new file mode 100644
index 0000000..ded0e61
--- /dev/null
+++ b/apps/api/src/database.py
@@ -0,0 +1,15 @@
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
+from sqlalchemy.pool import NullPool
+
+from src.config import Settings, build_database_url
+
+
+settings = Settings()
+engine = create_async_engine(
+ settings.database_url,
+ future=True,
+ poolclass=NullPool,
+)
+SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
+
+__all__ = ["SessionLocal", "build_database_url", "engine", "settings"]
diff --git a/apps/api/src/main.py b/apps/api/src/main.py
new file mode 100644
index 0000000..f101daa
--- /dev/null
+++ b/apps/api/src/main.py
@@ -0,0 +1,8 @@
+from fastapi import FastAPI
+
+from src.api.auth import router as auth_router
+from src.api.projects import router as projects_router
+
+app = FastAPI(title="Headquarter API")
+app.include_router(auth_router)
+app.include_router(projects_router)
diff --git a/apps/api/src/models/__init__.py b/apps/api/src/models/__init__.py
new file mode 100644
index 0000000..1c3e90f
--- /dev/null
+++ b/apps/api/src/models/__init__.py
@@ -0,0 +1,9 @@
+from src.models.base import Base
+from src.models.git_repository import GitRepository
+from src.models.project import Project
+from src.models.refresh_token import RefreshToken
+from src.models.ssh_key import SSHKey
+from src.models.user import User
+from src.models.user_config import UserConfig
+
+__all__ = ["Base", "GitRepository", "Project", "RefreshToken", "SSHKey", "User", "UserConfig"]
diff --git a/apps/api/src/models/base.py b/apps/api/src/models/base.py
new file mode 100644
index 0000000..fbf5edf
--- /dev/null
+++ b/apps/api/src/models/base.py
@@ -0,0 +1,24 @@
+import uuid
+from datetime import datetime
+
+from sqlalchemy import DateTime, func
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
+
+
+class Base(DeclarativeBase):
+ pass
+
+
+class UUIDPrimaryKeyMixin:
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+
+
+class TimestampMixin:
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ onupdate=func.now(),
+ nullable=False,
+ )
diff --git a/apps/api/src/models/git_repository.py b/apps/api/src/models/git_repository.py
new file mode 100644
index 0000000..d6876ca
--- /dev/null
+++ b/apps/api/src/models/git_repository.py
@@ -0,0 +1,28 @@
+import uuid
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Boolean, DateTime, ForeignKey, String
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
+
+if TYPE_CHECKING:
+ from src.models.project import Project
+ from src.models.user import User
+
+
+class GitRepository(UUIDPrimaryKeyMixin, TimestampMixin, Base):
+ __tablename__ = "git_repositories"
+
+ name: Mapped[str] = mapped_column(String(255))
+ path: Mapped[str] = mapped_column(String(1024))
+ project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
+ owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
+ is_mirror: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ remote_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
+ last_push: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+
+ project: Mapped["Project"] = relationship(back_populates="repositories")
+ owner: Mapped["User"] = relationship()
diff --git a/apps/api/src/models/project.py b/apps/api/src/models/project.py
new file mode 100644
index 0000000..977c5f7
--- /dev/null
+++ b/apps/api/src/models/project.py
@@ -0,0 +1,31 @@
+import uuid
+from typing import TYPE_CHECKING
+
+from sqlalchemy import ForeignKey, String, Text
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
+
+if TYPE_CHECKING:
+ from src.models.git_repository import GitRepository
+ from src.models.ssh_key import SSHKey
+ from src.models.user import User
+
+
+class Project(UUIDPrimaryKeyMixin, TimestampMixin, Base):
+ __tablename__ = "projects"
+
+ name: Mapped[str] = mapped_column(String(255))
+ description: Mapped[str | None] = mapped_column(Text, nullable=True)
+ owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
+ default_ssh_key_id: Mapped[uuid.UUID | None] = mapped_column(
+ UUID(as_uuid=True),
+ ForeignKey("ssh_keys.id"),
+ nullable=True,
+ )
+
+ owner: Mapped["User"] = relationship(back_populates="projects")
+ repositories: Mapped[list["GitRepository"]] = relationship(back_populates="project")
+ default_ssh_key: Mapped["SSHKey | None"] = relationship(foreign_keys=[default_ssh_key_id])
+ ssh_keys: Mapped[list["SSHKey"]] = relationship(back_populates="project", foreign_keys="SSHKey.project_id")
diff --git a/apps/api/src/models/refresh_token.py b/apps/api/src/models/refresh_token.py
new file mode 100644
index 0000000..44a34fb
--- /dev/null
+++ b/apps/api/src/models/refresh_token.py
@@ -0,0 +1,24 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from sqlalchemy import DateTime, ForeignKey, String
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from src.models.base import Base, UUIDPrimaryKeyMixin
+
+if TYPE_CHECKING:
+ from src.models.user import User
+
+
+class RefreshToken(UUIDPrimaryKeyMixin, Base):
+ __tablename__ = "refresh_tokens"
+
+ user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
+ token_hash: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
+ expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
+ revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+ user_agent: Mapped[str | None] = mapped_column(String(512), nullable=True)
+ ip_address: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
+
+ user: Mapped["User"] = relationship(back_populates="refresh_tokens")
diff --git a/apps/api/src/models/ssh_key.py b/apps/api/src/models/ssh_key.py
new file mode 100644
index 0000000..ce499ee
--- /dev/null
+++ b/apps/api/src/models/ssh_key.py
@@ -0,0 +1,25 @@
+import uuid
+from typing import TYPE_CHECKING
+
+from sqlalchemy import ForeignKey, String, Text
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from src.models.base import Base, UUIDPrimaryKeyMixin
+
+if TYPE_CHECKING:
+ from src.models.project import Project
+ from src.models.user import User
+
+
+class SSHKey(UUIDPrimaryKeyMixin, Base):
+ __tablename__ = "ssh_keys"
+
+ name: Mapped[str] = mapped_column(String(255))
+ public_key: Mapped[str] = mapped_column(Text)
+ private_key_encrypted: Mapped[str] = mapped_column(Text)
+ user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
+ project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=True)
+
+ user: Mapped["User"] = relationship(back_populates="ssh_keys")
+ project: Mapped["Project | None"] = relationship(back_populates="ssh_keys", foreign_keys=[project_id])
diff --git a/apps/api/src/models/user.py b/apps/api/src/models/user.py
new file mode 100644
index 0000000..c944884
--- /dev/null
+++ b/apps/api/src/models/user.py
@@ -0,0 +1,26 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import String
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
+
+if TYPE_CHECKING:
+ from src.models.project import Project
+ from src.models.refresh_token import RefreshToken
+ from src.models.ssh_key import SSHKey
+ from src.models.user_config import UserConfig
+
+
+class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
+ __tablename__ = "users"
+
+ email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
+ name: Mapped[str] = mapped_column(String(255))
+ authentik_id: Mapped[str] = mapped_column(String(255), unique=True, index=True)
+ avatar_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
+
+ projects: Mapped[list["Project"]] = relationship(back_populates="owner")
+ refresh_tokens: Mapped[list["RefreshToken"]] = relationship(back_populates="user")
+ ssh_keys: Mapped[list["SSHKey"]] = relationship(back_populates="user")
+ user_config: Mapped["UserConfig | None"] = relationship(back_populates="user", uselist=False)
diff --git a/apps/api/src/models/user_config.py b/apps/api/src/models/user_config.py
new file mode 100644
index 0000000..f7c46a5
--- /dev/null
+++ b/apps/api/src/models/user_config.py
@@ -0,0 +1,20 @@
+import uuid
+from typing import TYPE_CHECKING
+
+from sqlalchemy import ForeignKey
+from sqlalchemy.dialects.postgresql import JSONB, UUID
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
+
+if TYPE_CHECKING:
+ from src.models.user import User
+
+
+class UserConfig(UUIDPrimaryKeyMixin, TimestampMixin, Base):
+ __tablename__ = "user_configs"
+
+ user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True)
+ config: Mapped[dict[str, object]] = mapped_column(JSONB, default=dict, nullable=False)
+
+ user: Mapped["User"] = relationship(back_populates="user_config")
diff --git a/apps/api/src/scripts/__init__.py b/apps/api/src/scripts/__init__.py
new file mode 100644
index 0000000..a3d8f30
--- /dev/null
+++ b/apps/api/src/scripts/__init__.py
@@ -0,0 +1 @@
+"""Utility scripts for the API package."""
diff --git a/apps/api/src/scripts/seed.py b/apps/api/src/scripts/seed.py
new file mode 100644
index 0000000..6a4532d
--- /dev/null
+++ b/apps/api/src/scripts/seed.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src.database import SessionLocal
+from src.models.user import User
+
+
+def build_seed_user() -> Mapping[str, str | None]:
+ return {
+ "email": "dev@headquarter.local",
+ "name": "Development User",
+ "authentik_id": "dev-authentik-user",
+ "avatar_url": None,
+ }
+
+
+async def seed_database(session: AsyncSession) -> User:
+ payload = build_seed_user()
+ existing_user = await session.scalar(select(User).where(User.email == payload["email"]))
+
+ if existing_user is not None:
+ return existing_user
+
+ user = User(**payload)
+ session.add(user)
+ await session.commit()
+ await session.refresh(user)
+ return user
+
+
+async def run() -> None:
+ async with SessionLocal() as session:
+ user = await seed_database(session)
+ print({"user_id": str(user.id), "email": user.email})
+
+
+def main() -> None:
+ import asyncio
+
+ asyncio.run(run())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/apps/api/tests/test_auth_api.py b/apps/api/tests/test_auth_api.py
new file mode 100644
index 0000000..7061f4a
--- /dev/null
+++ b/apps/api/tests/test_auth_api.py
@@ -0,0 +1,218 @@
+import uuid
+from datetime import UTC, datetime, timedelta
+import asyncio
+import importlib
+
+from fastapi.testclient import TestClient
+import pytest
+from sqlalchemy import text
+from sqlalchemy.ext.asyncio import create_async_engine
+
+from src.auth.jwt_service import mint_access_token
+from src.config import Settings, build_database_url
+from src.models import Base
+from src.models.user import User
+
+
+@pytest.fixture(autouse=True)
+def configure_local_database(monkeypatch) -> None:
+ local_url = build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ monkeypatch.setenv("DATABASE_URL", local_url)
+
+
+def _prepare_auth_test_db() -> None:
+ async def _run() -> None:
+ engine = create_async_engine(
+ build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ )
+ async with engine.begin() as connection:
+ await connection.run_sync(Base.metadata.create_all)
+ await connection.execute(text("TRUNCATE TABLE refresh_tokens, users RESTART IDENTITY CASCADE"))
+ await engine.dispose()
+
+ asyncio.run(_run())
+
+
+def _load_app():
+ import src.database as database_module
+ import src.api.auth as auth_module
+ import src.main as main_module
+
+ importlib.reload(database_module)
+ importlib.reload(auth_module)
+ importlib.reload(main_module)
+ return main_module.app
+
+
+def _insert_user_for_refresh(user_id: str) -> None:
+ async def _run() -> None:
+ engine = create_async_engine(
+ build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ )
+ async with engine.begin() as connection:
+ await connection.run_sync(Base.metadata.create_all)
+ from sqlalchemy.ext.asyncio import async_sessionmaker
+
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
+ async with session_factory() as session:
+ user = User(
+ id=uuid.UUID(user_id),
+ email="refresh@headquarter.local",
+ name="Refresh User",
+ authentik_id="refresh-user",
+ avatar_url=None,
+ )
+ await session.merge(user)
+ await session.commit()
+ await engine.dispose()
+
+ asyncio.run(_run())
+
+
+def test_login_redirects_to_authentik_authorize_endpoint() -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ client = TestClient(app)
+ response = client.get("/auth/login", follow_redirects=False)
+
+ assert response.status_code == 307
+ assert "response_type=code" in response.headers["location"]
+
+
+def test_me_returns_401_without_access_cookie() -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ client = TestClient(app)
+ response = client.get("/auth/me")
+
+ assert response.status_code == 401
+
+
+def test_me_returns_user_payload_with_valid_access_cookie() -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ settings = Settings()
+ token = mint_access_token(
+ settings=settings,
+ subject=str(uuid.uuid4()),
+ email="dev@headquarter.local",
+ name="Dev User",
+ expires_at=datetime.now(UTC) + timedelta(minutes=15),
+ )
+
+ client = TestClient(app)
+ client.cookies.set("access_token", token)
+ response = client.get("/auth/me")
+
+ assert response.status_code == 200
+ assert response.json()["email"] == "dev@headquarter.local"
+
+
+def test_logout_clears_auth_cookies() -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ client = TestClient(app)
+ client.cookies.set("refresh_token", "opaque-token")
+ response = client.post("/auth/logout")
+
+ assert response.status_code == 200
+ assert "access_token=" in response.headers.get("set-cookie", "")
+
+
+def test_callback_rejects_mismatched_state() -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ client = TestClient(app)
+ client.cookies.set("auth_state", "expected")
+ response = client.get("/auth/callback?code=test-code&state=wrong")
+
+ assert response.status_code == 401
+
+
+def test_callback_sets_auth_cookies_after_success(monkeypatch) -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ async def fake_exchange_code_for_tokens(*, settings, code, redirect_uri, client):
+ return {"access_token": "provider-access", "refresh_token": "provider-refresh"}
+
+ def fake_verify_provider_access_token(*, settings, token, jwks):
+ return {"sub": "auth-sub-1", "email": "callback@headquarter.local", "name": "Callback User"}
+
+ async def fake_fetch_jwks(*, settings, client):
+ return {"keys": []}
+
+ monkeypatch.setattr("src.api.auth.exchange_code_for_tokens", fake_exchange_code_for_tokens)
+ monkeypatch.setattr("src.api.auth.verify_provider_access_token", fake_verify_provider_access_token)
+ monkeypatch.setattr("src.api.auth.fetch_jwks", fake_fetch_jwks)
+
+ client = TestClient(app)
+ client.cookies.set("auth_state", "good-state")
+ response = client.get("/auth/callback?code=valid-code&state=good-state")
+
+ assert response.status_code == 200
+ assert response.json()["email"] == "callback@headquarter.local"
+ set_cookie_header = response.headers.get("set-cookie", "")
+ assert "access_token=" in set_cookie_header
+ assert "refresh_token=" in set_cookie_header
+
+
+def test_refresh_rotates_cookie_and_returns_user_payload(monkeypatch) -> None:
+ _prepare_auth_test_db()
+ _insert_user_for_refresh("7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb")
+ app = _load_app()
+
+ async def fake_rotate_refresh_token(*, session, raw_token, user_agent, ip_address):
+ class StoredToken:
+ user_id = uuid.UUID("7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb")
+
+ return "new-refresh-token", StoredToken()
+
+ monkeypatch.setattr("src.api.auth.rotate_refresh_token", fake_rotate_refresh_token)
+
+ client = TestClient(app)
+ client.cookies.set("refresh_token", "old-refresh-token")
+ response = client.post("/auth/refresh")
+
+ assert response.status_code == 200
+ assert response.json()["sub"] == "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
+ assert "refresh_token=" in response.headers.get("set-cookie", "")
+
+
+def test_refresh_returns_401_for_invalid_refresh_token(monkeypatch) -> None:
+ _prepare_auth_test_db()
+ app = _load_app()
+
+ async def fake_rotate_refresh_token(*, session, raw_token, user_agent, ip_address):
+ raise ValueError("refresh token not found")
+
+ monkeypatch.setattr("src.api.auth.rotate_refresh_token", fake_rotate_refresh_token)
+
+ client = TestClient(app)
+ client.cookies.set("refresh_token", "invalid")
+ response = client.post("/auth/refresh")
+
+ assert response.status_code == 401
diff --git a/apps/api/tests/test_auth_services.py b/apps/api/tests/test_auth_services.py
new file mode 100644
index 0000000..1cc5160
--- /dev/null
+++ b/apps/api/tests/test_auth_services.py
@@ -0,0 +1,211 @@
+from datetime import UTC, datetime, timedelta
+import base64
+
+import httpx
+import pytest
+import pytest_asyncio
+from sqlalchemy import text
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
+
+from src.auth.cookies import build_cookie_options
+from src.auth.jwt_service import decode_access_token, mint_access_token
+from src.auth.oidc import build_login_redirect_url, exchange_code_for_tokens, verify_provider_access_token
+from src.auth.refresh_store import create_refresh_token, hash_refresh_token, revoke_refresh_token, rotate_refresh_token
+from src.config import Settings, build_database_url
+from src.models import Base
+from src.models.user import User
+
+
+def test_cookie_options_follow_environment_defaults(monkeypatch) -> None:
+ monkeypatch.setenv("APP_ENV", "development")
+ dev_settings = Settings()
+ dev_options = build_cookie_options(dev_settings)
+
+ monkeypatch.setenv("APP_ENV", "production")
+ prod_settings = Settings()
+ prod_options = build_cookie_options(prod_settings)
+
+ assert dev_options["httponly"] is True
+ assert dev_options["secure"] is False
+ assert dev_options["samesite"] == "lax"
+ assert prod_options["secure"] is True
+ assert prod_options["samesite"] == "strict"
+
+
+def test_login_redirect_url_contains_required_oidc_params() -> None:
+ settings = Settings()
+
+ url = build_login_redirect_url(
+ settings=settings,
+ redirect_uri="http://localhost:8000/auth/callback",
+ state="state-123",
+ nonce="nonce-123",
+ )
+
+ assert "response_type=code" in url
+ assert "client_id=headquarter-web" in url
+ assert "scope=openid+profile+email" in url
+ assert "state=state-123" in url
+ assert "nonce=nonce-123" in url
+
+
+def test_mint_and_decode_internal_access_token_round_trip() -> None:
+ settings = Settings()
+ expires_at = datetime.now(UTC) + timedelta(minutes=15)
+
+ token = mint_access_token(
+ settings=settings,
+ subject="user-123",
+ email="dev@headquarter.local",
+ name="Dev User",
+ expires_at=expires_at,
+ )
+
+ claims = decode_access_token(settings=settings, token=token)
+
+ assert claims["sub"] == "user-123"
+ assert claims["email"] == "dev@headquarter.local"
+ assert claims["name"] == "Dev User"
+ assert "exp" in claims
+
+
+def test_refresh_token_hash_is_deterministic_and_non_reversible() -> None:
+ raw_token = "refresh-token-abc"
+
+ first_hash = hash_refresh_token(raw_token)
+ second_hash = hash_refresh_token(raw_token)
+
+ assert first_hash == second_hash
+ assert first_hash != raw_token
+ assert len(first_hash) == 64
+
+
+def test_decode_access_token_rejects_invalid_signature() -> None:
+ settings = Settings()
+ other_settings = Settings(jwt_secret="different-secret")
+ expires_at = datetime.now(UTC) + timedelta(minutes=15)
+
+ token = mint_access_token(
+ settings=other_settings,
+ subject="user-123",
+ email="dev@headquarter.local",
+ name="Dev User",
+ expires_at=expires_at,
+ )
+
+ with pytest.raises(Exception):
+ decode_access_token(settings=settings, token=token)
+
+
+@pytest.mark.asyncio
+async def test_exchange_code_for_tokens_posts_expected_payload() -> None:
+ settings = Settings()
+
+ def handler(request: httpx.Request) -> httpx.Response:
+ assert request.url == httpx.URL(settings.authentik_token_url)
+ payload = dict(httpx.QueryParams(request.content.decode("utf-8")))
+ assert payload["grant_type"] == "authorization_code"
+ assert payload["code"] == "auth-code"
+ assert payload["redirect_uri"] == "http://localhost:8000/auth/callback"
+ return httpx.Response(200, json={"access_token": "provider-token", "refresh_token": "provider-refresh"})
+
+ transport = httpx.MockTransport(handler)
+ async with httpx.AsyncClient(transport=transport) as client:
+ token_payload = await exchange_code_for_tokens(
+ settings=settings,
+ code="auth-code",
+ redirect_uri="http://localhost:8000/auth/callback",
+ client=client,
+ )
+
+ assert token_payload["access_token"] == "provider-token"
+
+
+def test_verify_provider_access_token_with_jwks_oct_key() -> None:
+ settings = Settings(authentik_audience="headquarter-web", authentik_issuer="https://authentik.local/")
+ shared_secret = b"shared-secret-123"
+ jwks = {
+ "keys": [
+ {
+ "kty": "oct",
+ "alg": "HS256",
+ "k": base64.urlsafe_b64encode(shared_secret).decode("utf-8").rstrip("="),
+ "kid": "kid-1",
+ }
+ ]
+ }
+
+ from jose import jwt # type: ignore[import-untyped]
+
+ token = jwt.encode(
+ {
+ "sub": "authentik-user",
+ "iss": settings.authentik_issuer,
+ "aud": settings.authentik_audience,
+ "exp": int((datetime.now(UTC) + timedelta(minutes=5)).timestamp()),
+ },
+ shared_secret,
+ algorithm="HS256",
+ headers={"kid": "kid-1"},
+ )
+
+ claims = verify_provider_access_token(settings=settings, token=token, jwks=jwks)
+
+ assert claims["sub"] == "authentik-user"
+
+
+TEST_DATABASE_URL = build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+)
+
+
+@pytest_asyncio.fixture
+async def db_session() -> AsyncSession:
+ engine = create_async_engine(TEST_DATABASE_URL)
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
+
+ async with engine.begin() as connection:
+ await connection.run_sync(Base.metadata.create_all)
+
+ async with session_factory() as session:
+ await session.execute(text("TRUNCATE TABLE refresh_tokens, users RESTART IDENTITY CASCADE"))
+ await session.commit()
+ yield session
+ await session.rollback()
+
+ await engine.dispose()
+
+
+@pytest.mark.asyncio
+async def test_refresh_store_create_rotate_and_revoke(db_session: AsyncSession) -> None:
+ user = User(email="dev-auth@headquarter.local", name="Dev Auth", authentik_id="auth-dev", avatar_url=None)
+ db_session.add(user)
+ await db_session.commit()
+ await db_session.refresh(user)
+
+ raw_refresh_token, stored_token = await create_refresh_token(
+ session=db_session,
+ user_id=user.id,
+ expires_at=datetime.now(UTC) + timedelta(days=7),
+ user_agent="pytest",
+ ip_address="127.0.0.1",
+ )
+ assert raw_refresh_token
+ assert stored_token.revoked_at is None
+
+ rotated_raw, rotated_stored = await rotate_refresh_token(
+ session=db_session,
+ raw_token=raw_refresh_token,
+ user_agent="pytest-rotated",
+ ip_address="127.0.0.2",
+ )
+ assert rotated_raw != raw_refresh_token
+ assert rotated_stored.revoked_at is None
+ assert stored_token.revoked_at is not None
+
+ revoked = await revoke_refresh_token(session=db_session, raw_token=rotated_raw)
+ assert revoked is True
diff --git a/apps/api/tests/test_config.py b/apps/api/tests/test_config.py
new file mode 100644
index 0000000..69d976a
--- /dev/null
+++ b/apps/api/tests/test_config.py
@@ -0,0 +1,59 @@
+from src.config import Settings
+from src.database import build_database_url
+
+
+def test_settings_default_database_url_uses_asyncpg() -> None:
+ settings = Settings()
+
+ assert settings.database_url == "postgresql+asyncpg://headquarter:headquarter@postgres:5432/headquarter"
+
+
+def test_build_database_url_uses_explicit_values() -> None:
+ url = build_database_url(
+ user="user",
+ password="pass",
+ host="db",
+ port=5433,
+ database="app",
+ )
+
+ assert url == "postgresql+asyncpg://user:pass@db:5433/app"
+
+
+def test_settings_prefers_explicit_database_url_env(monkeypatch) -> None:
+ monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://local:local@localhost:5432/localdb")
+
+ settings = Settings()
+
+ assert settings.database_url == "postgresql+asyncpg://local:local@localhost:5432/localdb"
+
+
+def test_auth_settings_have_secure_defaults() -> None:
+ settings = Settings()
+
+ assert settings.authentik_client_id == "headquarter-web"
+ assert settings.authentik_client_secret == "change-me"
+ assert settings.authentik_authorize_url.endswith("/application/o/authorize/")
+ assert settings.authentik_token_url.endswith("/application/o/token/")
+ assert settings.authentik_jwks_url.endswith("/application/o/headquarter-web/jwks/")
+ assert settings.jwt_algorithm == "HS256"
+ assert settings.access_token_ttl_minutes == 15
+ assert settings.refresh_token_ttl_days == 7
+
+
+def test_cookie_policy_is_strict_in_production(monkeypatch) -> None:
+ monkeypatch.setenv("APP_ENV", "production")
+
+ settings = Settings()
+
+ assert settings.cookie_secure is True
+ assert settings.cookie_samesite == "strict"
+
+
+def test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) -> None:
+ monkeypatch.setenv("APP_ENV", "development")
+
+ settings = Settings()
+
+ assert settings.cookie_secure is False
+ assert settings.cookie_samesite == "lax"
diff --git a/apps/api/tests/test_migration_metadata.py b/apps/api/tests/test_migration_metadata.py
new file mode 100644
index 0000000..d40d4a5
--- /dev/null
+++ b/apps/api/tests/test_migration_metadata.py
@@ -0,0 +1,35 @@
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+
+
+def test_initial_migration_defines_all_core_tables() -> None:
+ migration_path = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0001_initial_schema.py"
+ spec = spec_from_file_location("initial_schema", migration_path)
+
+ assert spec is not None
+ assert spec.loader is not None
+
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ assert module.TABLE_NAMES == [
+ "users",
+ "ssh_keys",
+ "projects",
+ "git_repositories",
+ "user_configs",
+ ]
+
+
+def test_refresh_tokens_migration_has_expected_revision_chain() -> None:
+ migration_path = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0002_refresh_tokens.py"
+ spec = spec_from_file_location("refresh_tokens", migration_path)
+
+ assert spec is not None
+ assert spec.loader is not None
+
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ assert module.revision == "0002_refresh_tokens"
+ assert module.down_revision == "0001_initial_schema"
diff --git a/apps/api/tests/test_models.py b/apps/api/tests/test_models.py
new file mode 100644
index 0000000..ca032bc
--- /dev/null
+++ b/apps/api/tests/test_models.py
@@ -0,0 +1,153 @@
+from collections.abc import AsyncIterator
+
+import pytest
+import pytest_asyncio
+from sqlalchemy import text
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
+
+from src.config import build_database_url
+from src.models import Base
+from src.models.base import TimestampMixin, UUIDPrimaryKeyMixin
+from src.models.git_repository import GitRepository
+from src.models.project import Project
+from src.models.refresh_token import RefreshToken
+from src.models.ssh_key import SSHKey
+from src.models.user import User
+from src.models.user_config import UserConfig
+
+
+TEST_DATABASE_URL = build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+)
+
+
+@pytest_asyncio.fixture
+async def db_session() -> AsyncIterator[AsyncSession]:
+ engine = create_async_engine(TEST_DATABASE_URL)
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
+
+ async with session_factory() as session:
+ table_rows = await session.execute(
+ text(
+ "SELECT tablename FROM pg_tables "
+ "WHERE schemaname = 'public' "
+ "AND tablename = ANY(:table_names)"
+ ),
+ {
+ "table_names": [
+ "refresh_tokens",
+ "user_configs",
+ "git_repositories",
+ "projects",
+ "ssh_keys",
+ "users",
+ ]
+ },
+ )
+ existing_tables = [row[0] for row in table_rows]
+ if existing_tables:
+ await session.execute(text(f"TRUNCATE TABLE {', '.join(existing_tables)} RESTART IDENTITY CASCADE"))
+ await session.commit()
+ yield session
+ await session.rollback()
+
+ await engine.dispose()
+
+
+def test_base_metadata_collects_declared_tables() -> None:
+ assert isinstance(Base.metadata.tables, dict)
+
+
+def test_shared_mixins_define_expected_columns() -> None:
+ assert "id" in UUIDPrimaryKeyMixin.__dict__
+ assert "created_at" in TimestampMixin.__dict__
+ assert "updated_at" in TimestampMixin.__dict__
+
+
+def test_expected_tables_are_registered() -> None:
+ assert set(Base.metadata.tables) == {
+ "refresh_tokens",
+ "git_repositories",
+ "projects",
+ "ssh_keys",
+ "user_configs",
+ "users",
+ }
+
+
+def test_user_table_has_required_columns() -> None:
+ columns = User.__table__.columns
+
+ assert set(columns.keys()) == {
+ "id",
+ "email",
+ "name",
+ "authentik_id",
+ "avatar_url",
+ "created_at",
+ "updated_at",
+ }
+ assert columns["email"].unique is True
+ assert columns["authentik_id"].unique is True
+ assert columns["avatar_url"].nullable is True
+
+
+def test_project_relationships_point_to_owner_and_default_ssh_key() -> None:
+ owner_fk = next(iter(Project.__table__.c.owner_id.foreign_keys))
+ ssh_fk = next(iter(Project.__table__.c.default_ssh_key_id.foreign_keys))
+
+ assert owner_fk.target_fullname == "users.id"
+ assert ssh_fk.target_fullname == "ssh_keys.id"
+ assert Project.owner.property.mapper.class_ is User
+ assert Project.default_ssh_key.property.mapper.class_ is SSHKey
+
+
+def test_repository_and_user_config_relationships_are_registered() -> None:
+ project_fk = next(iter(GitRepository.__table__.c.project_id.foreign_keys))
+ owner_fk = next(iter(GitRepository.__table__.c.owner_id.foreign_keys))
+ user_config_fk = next(iter(UserConfig.__table__.c.user_id.foreign_keys))
+
+ assert project_fk.target_fullname == "projects.id"
+ assert owner_fk.target_fullname == "users.id"
+ assert user_config_fk.target_fullname == "users.id"
+ assert GitRepository.project.property.mapper.class_ is Project
+ assert GitRepository.owner.property.mapper.class_ is User
+ assert UserConfig.user.property.mapper.class_ is User
+
+
+def test_refresh_token_table_has_required_columns_and_relationships() -> None:
+ columns = RefreshToken.__table__.columns
+ user_fk = next(iter(RefreshToken.__table__.c.user_id.foreign_keys))
+
+ assert set(columns.keys()) == {
+ "id",
+ "user_id",
+ "token_hash",
+ "expires_at",
+ "revoked_at",
+ "user_agent",
+ "ip_address",
+ "created_at",
+ }
+ assert columns["token_hash"].unique is True
+ assert columns["revoked_at"].nullable is True
+ assert user_fk.target_fullname == "users.id"
+ assert RefreshToken.user.property.mapper.class_ is User
+
+
+@pytest.mark.asyncio
+async def test_async_session_can_insert_and_load_user(db_session: AsyncSession) -> None:
+ user = User(email="dev@headquarter.local", name="Dev User", authentik_id="dev-user", avatar_url=None)
+
+ db_session.add(user)
+ await db_session.commit()
+ await db_session.refresh(user)
+
+ loaded_user = await db_session.get(User, user.id)
+
+ assert loaded_user is not None
+ assert loaded_user.email == "dev@headquarter.local"
diff --git a/apps/api/tests/test_projects_api.py b/apps/api/tests/test_projects_api.py
new file mode 100644
index 0000000..92ac2cc
--- /dev/null
+++ b/apps/api/tests/test_projects_api.py
@@ -0,0 +1,270 @@
+import uuid
+from datetime import UTC, datetime, timedelta
+import asyncio
+
+from fastapi.testclient import TestClient
+import pytest
+from sqlalchemy import text
+from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
+
+from src.auth.jwt_service import mint_access_token
+from src.config import Settings, build_database_url
+from src.models import Base
+from src.models.project import Project
+from src.models.user import User
+
+
+@pytest.fixture(autouse=True)
+def configure_local_database(monkeypatch) -> None:
+ local_url = build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ monkeypatch.setenv("DATABASE_URL", local_url)
+
+
+def _prepare_test_db() -> None:
+ async def _run() -> None:
+ engine = create_async_engine(
+ build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ )
+ async with engine.begin() as connection:
+ await connection.run_sync(Base.metadata.create_all)
+ await connection.execute(text("TRUNCATE TABLE git_repositories, ssh_keys, projects, users RESTART IDENTITY CASCADE"))
+ await engine.dispose()
+
+ asyncio.run(_run())
+
+
+def _load_app():
+ import importlib
+ import src.database as database_module
+ import src.api.auth as auth_module
+ import src.api.projects as projects_module
+ import src.main as main_module
+
+ # Dispose old engine connections before reload to prevent pool exhaustion
+ if hasattr(database_module, 'engine'):
+ import asyncio
+ asyncio.run(database_module.engine.dispose())
+
+ importlib.reload(database_module)
+ importlib.reload(auth_module)
+ importlib.reload(projects_module)
+ importlib.reload(main_module)
+ return main_module.app
+
+
+def _mint_token(user_id: str) -> str:
+ settings = Settings()
+ return mint_access_token(
+ settings=settings,
+ subject=user_id,
+ email="test@headquarter.local",
+ name="Test User",
+ expires_at=datetime.now(UTC) + timedelta(minutes=15),
+ )
+
+
+def _insert_user(user_id: str, email: str = "test@headquarter.local") -> None:
+ async def _run() -> None:
+ engine = create_async_engine(
+ build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ )
+ async with engine.begin() as connection:
+ await connection.run_sync(Base.metadata.create_all)
+
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
+ async with session_factory() as session:
+ user = User(
+ id=uuid.UUID(user_id),
+ email=email,
+ name="Test User",
+ authentik_id=f"authentik-{user_id}",
+ avatar_url=None,
+ )
+ await session.merge(user)
+ await session.commit()
+ await engine.dispose()
+
+ asyncio.run(_run())
+
+
+def _insert_project(project_id: str, owner_id: str, name: str = "Test Project") -> None:
+ async def _run() -> None:
+ engine = create_async_engine(
+ build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+ )
+ )
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
+ async with session_factory() as session:
+ project = Project(
+ id=uuid.UUID(project_id),
+ name=name,
+ description="A test project",
+ owner_id=uuid.UUID(owner_id),
+ default_ssh_key_id=None,
+ )
+ await session.merge(project)
+ await session.commit()
+ await engine.dispose()
+
+ asyncio.run(_run())
+
+
+def test_create_project_requires_authentication() -> None:
+ _prepare_test_db()
+ app = _load_app()
+ client = TestClient(app)
+
+ response = client.post("/projects", json={"name": "New Project", "description": "Description"})
+
+ assert response.status_code == 401
+
+
+def test_create_project_successfully() -> None:
+ _prepare_test_db()
+ user_id = "11111111-1111-1111-1111-111111111111"
+ _insert_user(user_id)
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(user_id))
+
+ response = client.post("/projects", json={"name": "New Project", "description": "Description"})
+
+ assert response.status_code == 201
+ data = response.json()
+ assert data["name"] == "New Project"
+ assert data["description"] == "Description"
+ assert data["owner_id"] == user_id
+ assert "id" in data
+
+
+def test_list_projects_returns_only_owned_projects() -> None:
+ _prepare_test_db()
+ user1_id = "11111111-1111-1111-1111-111111111111"
+ user2_id = "22222222-2222-2222-2222-222222222222"
+ _insert_user(user1_id)
+ _insert_user(user2_id, "other@headquarter.local")
+ _insert_project("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", user1_id, "User1 Project")
+ _insert_project("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", user2_id, "User2 Project")
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(user1_id))
+
+ response = client.get("/projects")
+
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) == 1
+ assert data[0]["name"] == "User1 Project"
+
+
+def test_update_project_requires_ownership() -> None:
+ _prepare_test_db()
+ owner_id = "11111111-1111-1111-1111-111111111111"
+ other_id = "22222222-2222-2222-2222-222222222222"
+ project_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+ _insert_user(owner_id)
+ _insert_user(other_id, "other@headquarter.local")
+ _insert_project(project_id, owner_id)
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(other_id))
+
+ response = client.patch(f"/projects/{project_id}", json={"name": "Hacked"})
+
+ assert response.status_code == 403
+
+
+def test_update_project_successfully() -> None:
+ _prepare_test_db()
+ owner_id = "11111111-1111-1111-1111-111111111111"
+ project_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+ _insert_user(owner_id)
+ _insert_project(project_id, owner_id)
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(owner_id))
+
+ response = client.patch(f"/projects/{project_id}", json={"name": "Updated Name"})
+
+ assert response.status_code == 200
+ data = response.json()
+ assert data["name"] == "Updated Name"
+
+
+def test_delete_project_requires_ownership() -> None:
+ _prepare_test_db()
+ owner_id = "11111111-1111-1111-1111-111111111111"
+ other_id = "22222222-2222-2222-2222-222222222222"
+ project_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+ _insert_user(owner_id)
+ _insert_user(other_id, "other@headquarter.local")
+ _insert_project(project_id, owner_id)
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(other_id))
+
+ response = client.delete(f"/projects/{project_id}")
+
+ assert response.status_code == 403
+
+
+def test_delete_project_successfully() -> None:
+ _prepare_test_db()
+ owner_id = "11111111-1111-1111-1111-111111111111"
+ project_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+ _insert_user(owner_id)
+ _insert_project(project_id, owner_id)
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(owner_id))
+
+ response = client.delete(f"/projects/{project_id}")
+
+ assert response.status_code == 204
+
+
+def test_set_default_ssh_key_requires_ownership() -> None:
+ _prepare_test_db()
+ owner_id = "11111111-1111-1111-1111-111111111111"
+ other_id = "22222222-2222-2222-2222-222222222222"
+ project_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+ _insert_user(owner_id)
+ _insert_user(other_id, "other@headquarter.local")
+ _insert_project(project_id, owner_id)
+
+ app = _load_app()
+ client = TestClient(app)
+ client.cookies.set("access_token", _mint_token(other_id))
+
+ response = client.patch(f"/projects/{project_id}/default-ssh-key", json={"ssh_key_id": "cccccccc-cccc-cccc-cccc-cccccccccccc"})
+
+ assert response.status_code == 403
diff --git a/apps/api/tests/test_seed.py b/apps/api/tests/test_seed.py
new file mode 100644
index 0000000..d016827
--- /dev/null
+++ b/apps/api/tests/test_seed.py
@@ -0,0 +1,55 @@
+from collections.abc import AsyncIterator
+
+import pytest
+import pytest_asyncio
+from sqlalchemy import select, text
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
+
+from src.config import build_database_url
+from src.models.user import User
+from src.scripts.seed import build_seed_user, seed_database
+
+
+TEST_DATABASE_URL = build_database_url(
+ user="headquarter",
+ password="headquarter",
+ host="localhost",
+ port=5432,
+ database="headquarter",
+)
+
+
+@pytest_asyncio.fixture
+async def db_session() -> AsyncIterator[AsyncSession]:
+ engine = create_async_engine(TEST_DATABASE_URL)
+ session_factory = async_sessionmaker(engine, expire_on_commit=False)
+
+ async with session_factory() as session:
+ await session.execute(text("TRUNCATE TABLE git_repositories, projects, users RESTART IDENTITY CASCADE"))
+ await session.commit()
+ yield session
+ await session.execute(text("TRUNCATE TABLE git_repositories, projects, users RESTART IDENTITY CASCADE"))
+ await session.commit()
+
+ await engine.dispose()
+
+
+def test_build_seed_user_returns_deterministic_payload() -> None:
+ payload = build_seed_user()
+
+ assert payload == {
+ "email": "dev@headquarter.local",
+ "name": "Development User",
+ "authentik_id": "dev-authentik-user",
+ "avatar_url": None,
+ }
+
+
+@pytest.mark.asyncio
+async def test_seed_database_creates_development_user(db_session: AsyncSession) -> None:
+ await seed_database(db_session)
+
+ seeded_user = await db_session.scalar(select(User).where(User.email == "dev@headquarter.local"))
+
+ assert seeded_user is not None
+ assert seeded_user.authentik_id == "dev-authentik-user"
diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs
new file mode 100644
index 0000000..4d48425
--- /dev/null
+++ b/apps/web/.eslintrc.cjs
@@ -0,0 +1,16 @@
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ es2022: true
+ },
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module"
+ },
+ plugins: ["@typescript-eslint"],
+ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
+ ignorePatterns: ["dist", "node_modules"],
+ rules: {}
+};
diff --git a/apps/web/index.html b/apps/web/index.html
new file mode 100644
index 0000000..503025b
--- /dev/null
+++ b/apps/web/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Headquarter
+
+
+
+
+
+
diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json
new file mode 100644
index 0000000..22088b5
--- /dev/null
+++ b/apps/web/package-lock.json
@@ -0,0 +1,6334 @@
+{
+ "name": "headquarter-web",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "headquarter-web",
+ "version": "0.1.0",
+ "dependencies": {
+ "axios": "^1.6.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.20.0",
+ "tailwindcss": "^3.3.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
+ "@typescript-eslint/parser": "^6.14.0",
+ "@vitejs/plugin-react": "^4.2.0",
+ "autoprefixer": "^10.4.16",
+ "eslint": "^8.55.0",
+ "jsdom": "^29.1.1",
+ "postcss": "^8.4.32",
+ "typescript": "^5.3.0",
+ "vite": "^5.0.0",
+ "vitest": "^4.1.6"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
+ "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.2.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz",
+ "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
+ "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.130.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
+ "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
+ "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
+ "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
+ "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
+ "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
+ "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
+ "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
+ "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
+ "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
+ "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
+ "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
+ "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
+ "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
+ "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+ "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/type-utils": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+ "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
+ "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
+ "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.6",
+ "@vitest/utils": "4.1.6",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz",
+ "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz",
+ "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.6",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz",
+ "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.6",
+ "@vitest/utils": "4.1.6",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz",
+ "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz",
+ "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.6",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "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/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-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"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
+ "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+ "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/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.30",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz",
+ "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.357",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz",
+ "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "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/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.3.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
+ "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
+ "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.130.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.1",
+ "@rolldown/binding-darwin-arm64": "1.0.1",
+ "@rolldown/binding-darwin-x64": "1.0.1",
+ "@rolldown/binding-freebsd-x64": "1.0.1",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.1",
+ "@rolldown/binding-linux-arm64-musl": "1.0.1",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-musl": "1.0.1",
+ "@rolldown/binding-openharmony-arm64": "1.0.1",
+ "@rolldown/binding-wasm32-wasi": "1.0.1",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.1",
+ "@rolldown/binding-win32-x64-msvc": "1.0.1"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
+ "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.0.30",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz",
+ "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.30"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.30",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz",
+ "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
+ "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
+ "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.6",
+ "@vitest/mocker": "4.1.6",
+ "@vitest/pretty-format": "4.1.6",
+ "@vitest/runner": "4.1.6",
+ "@vitest/snapshot": "4.1.6",
+ "@vitest/spy": "4.1.6",
+ "@vitest/utils": "4.1.6",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.6",
+ "@vitest/browser-preview": "4.1.6",
+ "@vitest/browser-webdriverio": "4.1.6",
+ "@vitest/coverage-istanbul": "4.1.6",
+ "@vitest/coverage-v8": "4.1.6",
+ "@vitest/ui": "4.1.6",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz",
+ "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.6",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest/node_modules/vite": {
+ "version": "8.0.13",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
+ "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "rolldown": "1.0.1",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "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/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index 7f0c19c..7e1414c 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -7,25 +7,30 @@
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "test": "vitest run"
},
"dependencies": {
+ "axios": "^1.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
- "axios": "^1.6.0",
"tailwindcss": "^3.3.0"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
- "@vitejs/plugin-react": "^4.2.0",
- "typescript": "^5.3.0",
- "vite": "^5.0.0",
- "eslint": "^8.55.0",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
+ "@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
- "postcss": "^8.4.32"
+ "eslint": "^8.55.0",
+ "jsdom": "^29.1.1",
+ "postcss": "^8.4.32",
+ "typescript": "^5.3.0",
+ "vite": "^5.0.0",
+ "vitest": "^4.1.6"
}
}
diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts
new file mode 100644
index 0000000..f4754aa
--- /dev/null
+++ b/apps/web/src/api/client.ts
@@ -0,0 +1,26 @@
+import axios from "axios";
+
+const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
+
+export const apiClient = axios.create({
+ baseURL: BASE_URL,
+ withCredentials: true,
+ headers: {
+ "Content-Type": "application/json"
+ }
+});
+
+export const shouldSkipAuthRedirect = (path: string): boolean => {
+ return path.startsWith("/login") || path.startsWith("/auth");
+};
+
+apiClient.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ const status = error?.response?.status;
+ if (status === 401 && !shouldSkipAuthRedirect(window.location.pathname)) {
+ window.location.assign("/auth/login");
+ }
+ return Promise.reject(error);
+ }
+);
diff --git a/apps/web/src/api/dashboard.ts b/apps/web/src/api/dashboard.ts
new file mode 100644
index 0000000..a48dd9a
--- /dev/null
+++ b/apps/web/src/api/dashboard.ts
@@ -0,0 +1,13 @@
+import { apiClient } from "./client";
+
+export type DashboardSummary = {
+ projects: number;
+ repositories: number;
+ sshKeys: number;
+ recentActivity: string[];
+};
+
+export const getDashboardSummary = async (): Promise => {
+ const response = await apiClient.get("/dashboard/summary");
+ return response.data;
+};
diff --git a/apps/web/src/api/projects.ts b/apps/web/src/api/projects.ts
new file mode 100644
index 0000000..beaed85
--- /dev/null
+++ b/apps/web/src/api/projects.ts
@@ -0,0 +1,51 @@
+import { apiClient } from "./client";
+import type { Project } from "../types";
+
+export type ProjectCreateInput = {
+ name: string;
+ description?: string | null;
+};
+
+export type ProjectUpdateInput = {
+ name?: string | null;
+ description?: string | null;
+};
+
+export type SetDefaultSSHKeyInput = {
+ ssh_key_id: string;
+};
+
+export const listProjects = async (): Promise => {
+ const response = await apiClient.get("/projects");
+ return response.data;
+};
+
+export const createProject = async (
+ input: ProjectCreateInput
+): Promise => {
+ const response = await apiClient.post("/projects", input);
+ return response.data;
+};
+
+export const updateProject = async (
+ projectId: string,
+ input: ProjectUpdateInput
+): Promise => {
+ const response = await apiClient.patch(`/projects/${projectId}`, input);
+ return response.data;
+};
+
+export const deleteProject = async (projectId: string): Promise => {
+ await apiClient.delete(`/projects/${projectId}`);
+};
+
+export const setDefaultSSHKey = async (
+ projectId: string,
+ input: SetDefaultSSHKeyInput
+): Promise => {
+ const response = await apiClient.patch(
+ `/projects/${projectId}/default-ssh-key`,
+ input
+ );
+ return response.data;
+};
diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx
new file mode 100644
index 0000000..f16a98d
--- /dev/null
+++ b/apps/web/src/components/app-shell.tsx
@@ -0,0 +1,56 @@
+import { Link, NavLink, Outlet } from "react-router-dom";
+
+import { useAuth } from "../state/auth";
+
+const NAV_ITEMS = [
+ { to: "/", label: "Dashboard" },
+ { to: "/projects", label: "Projects" },
+ { to: "/repositories", label: "Repositories" },
+ { to: "/ssh-keys", label: "SSH Keys" },
+ { to: "/settings", label: "Settings" }
+];
+
+export const AppShell = () => {
+ const { user, logout } = useAuth();
+
+ return (
+
+
+
+ Headquarter
+
+
+
{user?.name ?? "User"}
+
{
+ void logout();
+ }}
+ type="button"
+ >
+ Logout
+
+
+
+
+
+
+ {NAV_ITEMS.map((item) => (
+ (isActive ? "nav-item nav-item-active" : "nav-item")}
+ end={item.to === "/"}
+ >
+ {item.label}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/protected-route.test.tsx b/apps/web/src/components/protected-route.test.tsx
new file mode 100644
index 0000000..d2fd135
--- /dev/null
+++ b/apps/web/src/components/protected-route.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen } from "@testing-library/react";
+import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { describe, expect, it, vi } from "vitest";
+
+import { ProtectedRoute } from "./protected-route";
+
+const mockUseAuth = vi.fn();
+
+vi.mock("../state/auth", () => ({
+ useAuth: () => mockUseAuth()
+}));
+
+describe("ProtectedRoute", () => {
+ it("shows loading while session is resolving", () => {
+ mockUseAuth.mockReturnValue({ state: "loading" });
+
+ render(
+
+
+ private content
+
+
+ );
+
+ expect(screen.getByText("Checking session...")).toBeInTheDocument();
+ });
+
+ it("redirects unauthenticated users to login", () => {
+ mockUseAuth.mockReturnValue({ state: "unauthenticated" });
+
+ render(
+
+
+
+ private content
+
+ }
+ />
+ login page} />
+
+
+ );
+
+ expect(screen.getByText("login page")).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/protected-route.tsx b/apps/web/src/components/protected-route.tsx
new file mode 100644
index 0000000..c0275dd
--- /dev/null
+++ b/apps/web/src/components/protected-route.tsx
@@ -0,0 +1,19 @@
+import { Navigate, useLocation } from "react-router-dom";
+
+import { useAuth } from "../state/auth";
+
+export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
+ const { state } = useAuth();
+ const location = useLocation();
+
+ if (state === "loading") {
+ return Checking session...
;
+ }
+
+ if (state === "unauthenticated") {
+ const nextPath = encodeURIComponent(location.pathname);
+ return ;
+ }
+
+ return <>{children}>;
+};
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
new file mode 100644
index 0000000..e95320a
--- /dev/null
+++ b/apps/web/src/main.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
+
+import { AppRouter } from "./router";
+import { AuthProvider } from "./state/auth";
+import "./styles.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+
+
+);
diff --git a/apps/web/src/pages/dashboard.test.tsx b/apps/web/src/pages/dashboard.test.tsx
new file mode 100644
index 0000000..480568f
--- /dev/null
+++ b/apps/web/src/pages/dashboard.test.tsx
@@ -0,0 +1,54 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { DashboardPage } from "./dashboard";
+
+const mockGet = vi.fn();
+
+vi.mock("../api/dashboard", () => ({
+ getDashboardSummary: (...args: unknown[]) => mockGet(...args)
+}));
+
+describe("DashboardPage", () => {
+ beforeEach(() => {
+ mockGet.mockReset();
+ });
+
+ it("shows loading then empty state when summary has no data", async () => {
+ mockGet.mockResolvedValue({
+ projects: 0,
+ repositories: 0,
+ sshKeys: 0,
+ recentActivity: []
+ });
+
+ render( );
+
+ expect(screen.getByText("Loading dashboard...")).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.getByText("No activity yet")).toBeInTheDocument();
+ });
+ });
+
+ it("shows retry action when summary request fails", async () => {
+ mockGet.mockRejectedValueOnce(new Error("failed"));
+ mockGet.mockResolvedValueOnce({
+ projects: 2,
+ repositories: 5,
+ sshKeys: 1,
+ recentActivity: ["Created repo"]
+ });
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText("Dashboard is unavailable")).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByRole("button", { name: "Retry" }));
+
+ await waitFor(() => {
+ expect(screen.getByText("2")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/apps/web/src/pages/dashboard.tsx b/apps/web/src/pages/dashboard.tsx
new file mode 100644
index 0000000..d7f7146
--- /dev/null
+++ b/apps/web/src/pages/dashboard.tsx
@@ -0,0 +1,79 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
+
+const CARDS = [
+ { label: "Projects", key: "projects" },
+ { label: "Repositories", key: "repositories" },
+ { label: "SSH Keys", key: "sshKeys" }
+] as const;
+
+type DashboardStatus = "loading" | "ready" | "error";
+
+export const DashboardPage = () => {
+ const [status, setStatus] = useState("loading");
+ const [summary, setSummary] = useState(null);
+
+ const loadSummary = useCallback(async () => {
+ setStatus("loading");
+ try {
+ const data = await getDashboardSummary();
+ setSummary(data);
+ setStatus("ready");
+ } catch {
+ setSummary(null);
+ setStatus("error");
+ }
+ }, []);
+
+ useEffect(() => {
+ void loadSummary();
+ }, [loadSummary]);
+
+ const cards = useMemo(() => CARDS, []);
+ const isEmpty =
+ status === "ready" &&
+ summary !== null &&
+ summary.projects === 0 &&
+ summary.repositories === 0 &&
+ summary.sshKeys === 0 &&
+ summary.recentActivity.length === 0;
+
+ return (
+
+ Dashboard
+ Your workspace overview will appear here.
+
+ {status === "loading" && Loading dashboard...
}
+
+ {status === "error" && (
+
+
Dashboard is unavailable
+
void loadSummary()} type="button">
+ Retry
+
+
+ )}
+
+
+ {cards.map((card) => (
+
+ {card.label}
+ {summary ? String(summary[card.key]) : "-"}
+
+ ))}
+
+
+ {isEmpty && No activity yet
}
+
+
+
+ New Project
+
+
+ Add Repository
+
+
+
+ );
+};
diff --git a/apps/web/src/pages/placeholder.tsx b/apps/web/src/pages/placeholder.tsx
new file mode 100644
index 0000000..6c11137
--- /dev/null
+++ b/apps/web/src/pages/placeholder.tsx
@@ -0,0 +1,32 @@
+export const PlaceholderPage = ({ title }: { title: string }) => {
+ return (
+
+ {title}
+ This page is part of the frontend foundation scaffold.
+
+ );
+};
+
+export const NotFoundPage = () => {
+ return (
+
+ 404
+ The page you requested does not exist.
+
+ );
+};
+
+export const LoginRedirectPage = () => {
+ const nextPath = new URLSearchParams(window.location.search).get("next") ?? "/";
+ const encodedNext = encodeURIComponent(nextPath);
+
+ return (
+
+ );
+};
diff --git a/apps/web/src/pages/projects.test.tsx b/apps/web/src/pages/projects.test.tsx
new file mode 100644
index 0000000..c351626
--- /dev/null
+++ b/apps/web/src/pages/projects.test.tsx
@@ -0,0 +1,163 @@
+import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { ProjectsPage } from "./projects";
+import * as projectsApi from "../api/projects";
+
+const mockProjects = [
+ {
+ id: "proj-1",
+ name: "Alpha Project",
+ description: "First project",
+ owner_id: "user-1",
+ default_ssh_key_id: null,
+ },
+ {
+ id: "proj-2",
+ name: "Beta Project",
+ description: null,
+ owner_id: "user-1",
+ default_ssh_key_id: null,
+ },
+];
+
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+describe("ProjectsPage", () => {
+ it("renders loading state initially", () => {
+ vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
+ render( );
+ expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
+ });
+
+ it("renders project list after loading", async () => {
+ vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText("Alpha Project")).toBeInTheDocument();
+ });
+ expect(screen.getByText("Beta Project")).toBeInTheDocument();
+ expect(screen.getByText("First project")).toBeInTheDocument();
+ });
+
+ it("renders empty state when no projects", async () => {
+ vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
+ });
+ });
+
+ it("renders error state with retry button", async () => {
+ vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText(/failed to load projects/i)).toBeInTheDocument();
+ });
+ expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
+ });
+
+ it("opens create dialog and submits new project", async () => {
+ const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
+ const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByRole("button", { name: /new project/i }));
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+
+ fireEvent.change(screen.getByPlaceholderText(/project name/i), {
+ target: { value: "Gamma Project" },
+ });
+ fireEvent.change(screen.getByPlaceholderText(/optional description/i), {
+ target: { value: "A new project" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: /create/i }));
+
+ await waitFor(() => {
+ expect(createMock).toHaveBeenCalledWith({
+ name: "Gamma Project",
+ description: "A new project",
+ });
+ });
+ expect(listMock).toHaveBeenCalledTimes(2);
+ });
+
+ it("shows validation error when name is empty", async () => {
+ vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByRole("button", { name: /new project/i }));
+ fireEvent.click(screen.getByRole("button", { name: /create/i }));
+
+ expect(screen.getByText(/project name is required/i)).toBeInTheDocument();
+ });
+
+ it("opens edit dialog and saves changes", async () => {
+ const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
+ const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText("Alpha Project")).toBeInTheDocument();
+ });
+
+ const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
+ if (!alphaCard) throw new Error("Card not found");
+
+ fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+
+ const nameInput = screen.getByDisplayValue("Alpha Project");
+ fireEvent.change(nameInput, { target: { value: "Alpha Updated" } });
+ fireEvent.click(screen.getByRole("button", { name: /save/i }));
+
+ await waitFor(() => {
+ expect(updateMock).toHaveBeenCalledWith("proj-1", {
+ name: "Alpha Updated",
+ description: "First project",
+ });
+ });
+ expect(listMock).toHaveBeenCalledTimes(2);
+ });
+
+ it("shows delete confirmation and deletes project", async () => {
+ const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
+ const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
+
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText("Alpha Project")).toBeInTheDocument();
+ });
+
+ const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
+ if (!alphaCard) throw new Error("Card not found");
+
+ fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
+ expect(within(alphaCard).getByText(/are you sure/i)).toBeInTheDocument();
+
+ fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
+
+ await waitFor(() => {
+ expect(deleteMock).toHaveBeenCalledWith("proj-1");
+ });
+ expect(listMock).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/apps/web/src/pages/projects.tsx b/apps/web/src/pages/projects.tsx
new file mode 100644
index 0000000..93b7753
--- /dev/null
+++ b/apps/web/src/pages/projects.tsx
@@ -0,0 +1,215 @@
+import { useCallback, useEffect, useState } from "react";
+
+import {
+ createProject,
+ deleteProject,
+ listProjects,
+ updateProject,
+ type ProjectCreateInput,
+ type ProjectUpdateInput,
+} from "../api/projects";
+import type { Project } from "../types";
+
+type ProjectsStatus = "loading" | "ready" | "error";
+type DialogMode = "none" | "create" | "edit";
+
+export const ProjectsPage = () => {
+ const [status, setStatus] = useState("loading");
+ const [projects, setProjects] = useState([]);
+ const [dialogMode, setDialogMode] = useState("none");
+ const [editingProject, setEditingProject] = useState(null);
+ const [formName, setFormName] = useState("");
+ const [formDescription, setFormDescription] = useState("");
+ const [formError, setFormError] = useState(null);
+ const [deleteConfirmId, setDeleteConfirmId] = useState(null);
+
+ const loadProjects = useCallback(async () => {
+ setStatus("loading");
+ try {
+ const data = await listProjects();
+ setProjects(data);
+ setStatus("ready");
+ } catch {
+ setProjects([]);
+ setStatus("error");
+ }
+ }, []);
+
+ useEffect(() => {
+ void loadProjects();
+ }, [loadProjects]);
+
+ const openCreate = () => {
+ setFormName("");
+ setFormDescription("");
+ setFormError(null);
+ setEditingProject(null);
+ setDialogMode("create");
+ };
+
+ const openEdit = (project: Project) => {
+ setFormName(project.name);
+ setFormDescription(project.description ?? "");
+ setFormError(null);
+ setEditingProject(project);
+ setDialogMode("edit");
+ };
+
+ const closeDialog = () => {
+ setDialogMode("none");
+ setEditingProject(null);
+ setFormError(null);
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setFormError(null);
+
+ if (!formName.trim()) {
+ setFormError("Project name is required");
+ return;
+ }
+
+ try {
+ if (dialogMode === "create") {
+ const input: ProjectCreateInput = {
+ name: formName.trim(),
+ description: formDescription.trim() || null,
+ };
+ await createProject(input);
+ } else if (dialogMode === "edit" && editingProject) {
+ const input: ProjectUpdateInput = {
+ name: formName.trim(),
+ description: formDescription.trim() || null,
+ };
+ await updateProject(editingProject.id, input);
+ }
+ closeDialog();
+ await loadProjects();
+ } catch {
+ setFormError("Failed to save project");
+ }
+ };
+
+ const handleDelete = async (projectId: string) => {
+ try {
+ await deleteProject(projectId);
+ setDeleteConfirmId(null);
+ await loadProjects();
+ } catch {
+ setDeleteConfirmId(null);
+ }
+ };
+
+ const isEmpty = status === "ready" && projects.length === 0;
+
+ return (
+
+
+
Projects
+
+ New Project
+
+
+
+ {status === "loading" && Loading projects...
}
+
+ {status === "error" && (
+
+
Failed to load projects
+
void loadProjects()} type="button">
+ Retry
+
+
+ )}
+
+ {isEmpty && No projects yet. Create your first project above.
}
+
+ {status === "ready" && projects.length > 0 && (
+
+ {projects.map((project) => (
+
+
+
{project.name}
+ {project.description &&
{project.description}
}
+
+
+
openEdit(project)}
+ type="button"
+ >
+ Edit
+
+ {deleteConfirmId === project.id ? (
+
+ Are you sure?
+ void handleDelete(project.id)}
+ type="button"
+ >
+ Delete
+
+ setDeleteConfirmId(null)}
+ type="button"
+ >
+ Cancel
+
+
+ ) : (
+
setDeleteConfirmId(project.id)}
+ type="button"
+ >
+ Delete
+
+ )}
+
+
+ ))}
+
+ )}
+
+ {dialogMode !== "none" && (
+
+
+
{dialogMode === "create" ? "Create Project" : "Edit Project"}
+
+
+
+ )}
+
+ );
+};
diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx
new file mode 100644
index 0000000..ce2d9ef
--- /dev/null
+++ b/apps/web/src/router.tsx
@@ -0,0 +1,31 @@
+import { Navigate, Route, Routes } from "react-router-dom";
+
+import { AppShell } from "./components/app-shell";
+import { ProtectedRoute } from "./components/protected-route";
+import { DashboardPage } from "./pages/dashboard";
+import { LoginRedirectPage, NotFoundPage, PlaceholderPage } from "./pages/placeholder";
+import { ProjectsPage } from "./pages/projects";
+
+export const AppRouter = () => {
+ return (
+
+ } />
+
+
+
+ }
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
+
+ );
+};
diff --git a/apps/web/src/state/auth.tsx b/apps/web/src/state/auth.tsx
new file mode 100644
index 0000000..9bf1102
--- /dev/null
+++ b/apps/web/src/state/auth.tsx
@@ -0,0 +1,63 @@
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
+
+import { apiClient } from "../api/client";
+import type { SessionPayload, SessionUser } from "../types";
+
+type AuthState = "loading" | "authenticated" | "unauthenticated";
+
+type AuthContextValue = {
+ state: AuthState;
+ user: SessionUser | null;
+ refreshSession: () => Promise;
+ logout: () => Promise;
+};
+
+const AuthContext = createContext(undefined);
+
+export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
+ const [state, setState] = useState("loading");
+ const [user, setUser] = useState(null);
+
+ const refreshSession = useCallback(async () => {
+ setState("loading");
+ try {
+ const response = await apiClient.get("/auth/me");
+ setUser(response.data.user);
+ setState("authenticated");
+ } catch {
+ setUser(null);
+ setState("unauthenticated");
+ }
+ }, []);
+
+ const logout = useCallback(async () => {
+ await apiClient.post("/auth/logout");
+ setUser(null);
+ setState("unauthenticated");
+ }, []);
+
+ useEffect(() => {
+ void refreshSession();
+ }, [refreshSession]);
+
+ const value = useMemo(
+ () => ({
+ state,
+ user,
+ refreshSession,
+ logout
+ }),
+ [refreshSession, state, user, logout]
+ );
+
+ return {children} ;
+};
+
+export const useAuth = (): AuthContextValue => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error("useAuth must be used within AuthProvider");
+ }
+
+ return context;
+};
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
new file mode 100644
index 0000000..b8d1589
--- /dev/null
+++ b/apps/web/src/styles.css
@@ -0,0 +1,296 @@
+:root {
+ color-scheme: light;
+ font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
+ --bg: #f4f1ea;
+ --panel: #fffef9;
+ --ink: #1d1d1b;
+ --muted: #5f5b55;
+ --brand: #275d4b;
+ --brand-strong: #154236;
+ --border: #d8d0c5;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ background: radial-gradient(circle at top right, #fff5d6, var(--bg));
+ color: var(--ink);
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.shell {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.shell-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.85rem 1.25rem;
+ border-bottom: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.85);
+ backdrop-filter: blur(7px);
+}
+
+.brand {
+ font-weight: 700;
+ letter-spacing: 0.02em;
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.shell-body {
+ display: grid;
+ grid-template-columns: 230px 1fr;
+ min-height: calc(100vh - 57px);
+}
+
+.shell-nav {
+ border-right: 1px solid var(--border);
+ padding: 1rem 0.75rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+}
+
+.nav-item {
+ padding: 0.65rem 0.75rem;
+ border-radius: 10px;
+ color: var(--muted);
+}
+
+.nav-item:hover {
+ background: #ece7df;
+ color: var(--ink);
+}
+
+.nav-item-active {
+ background: var(--brand);
+ color: #f7fff7;
+}
+
+.shell-content {
+ padding: 1.25rem;
+}
+
+.stack {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.card-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.9rem;
+}
+
+.card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 1rem;
+}
+
+.card-label {
+ margin: 0;
+ color: var(--muted);
+}
+
+.card-value {
+ margin: 0.45rem 0 0;
+ font-size: 1.6rem;
+ font-weight: 700;
+}
+
+.quick-actions {
+ display: flex;
+ gap: 0.6rem;
+}
+
+.primary-button,
+.secondary-button,
+.ghost-button {
+ border-radius: 10px;
+ border: 1px solid transparent;
+ padding: 0.58rem 0.85rem;
+ cursor: pointer;
+ font: inherit;
+}
+
+.primary-button {
+ background: var(--brand);
+ color: white;
+}
+
+.primary-button:hover {
+ background: var(--brand-strong);
+}
+
+.secondary-button {
+ border-color: var(--border);
+ background: var(--panel);
+}
+
+.ghost-button {
+ border-color: var(--border);
+ background: transparent;
+}
+
+.user-chip {
+ border: 1px solid var(--border);
+ background: var(--panel);
+ border-radius: 999px;
+ padding: 0.35rem 0.7rem;
+ font-size: 0.9rem;
+}
+
+.center-screen {
+ min-height: 55vh;
+ display: grid;
+ place-content: center;
+ text-align: center;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.project-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.project-card {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+}
+
+.project-info h3 {
+ margin: 0 0 0.35rem;
+}
+
+.project-info p {
+ margin: 0;
+}
+
+.project-actions {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.delete-confirm {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.danger-button {
+ border-radius: 10px;
+ border: 1px solid transparent;
+ padding: 0.58rem 0.85rem;
+ cursor: pointer;
+ font: inherit;
+ background: #b91c1c;
+ color: white;
+}
+
+.danger-text {
+ color: #b91c1c;
+}
+
+.error-text {
+ color: #b91c1c;
+ margin: 0;
+}
+
+.dialog-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.35);
+ display: grid;
+ place-content: center;
+ z-index: 50;
+}
+
+.dialog {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 1.25rem;
+ min-width: 320px;
+ max-width: 90vw;
+}
+
+.dialog h2 {
+ margin: 0 0 1rem;
+}
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.form-field input,
+.form-field textarea {
+ padding: 0.55rem 0.7rem;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ font: inherit;
+}
+
+.dialog-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.6rem;
+}
+
+@media (max-width: 860px) {
+ .shell-body {
+ grid-template-columns: 1fr;
+ }
+
+ .shell-nav {
+ flex-direction: row;
+ overflow-x: auto;
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .card-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .quick-actions {
+ flex-direction: column;
+ }
+
+ .project-card {
+ flex-direction: column;
+ }
+}
diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts
new file mode 100644
index 0000000..f149f27
--- /dev/null
+++ b/apps/web/src/test/setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts
new file mode 100644
index 0000000..bbe7b8b
--- /dev/null
+++ b/apps/web/src/types.ts
@@ -0,0 +1,17 @@
+export type SessionUser = {
+ id: string;
+ email: string;
+ name: string;
+};
+
+export type SessionPayload = {
+ user: SessionUser;
+};
+
+export type Project = {
+ id: string;
+ name: string;
+ description: string | null;
+ owner_id: string;
+ default_ssh_key_id: string | null;
+};
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..5dad5fe
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src"]
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..fd3561a
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173
+ },
+ test: {
+ environment: "jsdom",
+ setupFiles: "./src/test/setup.ts"
+ }
+});
diff --git a/docs/superpowers/plans/2026-05-17-auth-oauth.md b/docs/superpowers/plans/2026-05-17-auth-oauth.md
new file mode 100644
index 0000000..2ed5942
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-17-auth-oauth.md
@@ -0,0 +1,65 @@
+# Auth OAuth Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Implement Authentik-backed OIDC login that issues internal JWT access tokens, rotates DB-backed refresh tokens, and supports secure logout.
+
+**Architecture:** FastAPI route handlers delegate to focused auth services: OIDC provider client, token verifier/minting service, and refresh token store. Session state is carried in httpOnly cookies while refresh-token validity is enforced from PostgreSQL. Token trust boundary is explicit: provider token is JWKS-verified before local token minting.
+
+**Tech Stack:** FastAPI, SQLAlchemy (async), Alembic, python-jose, httpx, pytest/pytest-asyncio, ruff, mypy
+
+---
+
+### Task 1: Config and DB schema
+
+**Files:**
+- Modify: `apps/api/src/config.py`
+- Modify: `apps/api/src/models/user.py`
+- Create: `apps/api/src/models/refresh_token.py`
+- Modify: `apps/api/src/models/__init__.py`
+- Create: `apps/api/alembic/versions/0002_refresh_tokens.py`
+- Test: `apps/api/tests/test_config.py`
+- Test: `apps/api/tests/test_models.py`
+
+- [ ] **Step 1: Write failing tests for OIDC/JWT config and refresh-token metadata**
+- [ ] **Step 2: Run focused tests to verify red state**
+- [ ] **Step 3: Implement minimal config and model changes**
+- [ ] **Step 4: Add migration and migration metadata test updates**
+- [ ] **Step 5: Re-run focused tests to verify green state**
+
+### Task 2: Auth services
+
+**Files:**
+- Create: `apps/api/src/auth/__init__.py`
+- Create: `apps/api/src/auth/cookies.py`
+- Create: `apps/api/src/auth/oidc.py`
+- Create: `apps/api/src/auth/jwt_service.py`
+- Create: `apps/api/src/auth/refresh_store.py`
+- Test: `apps/api/tests/test_auth_services.py`
+
+- [ ] **Step 1: Write failing tests for cookie policy, JWT mint/verify, and refresh lifecycle**
+- [ ] **Step 2: Run targeted tests to verify failures are expected**
+- [ ] **Step 3: Implement minimal auth service modules to satisfy tests**
+- [ ] **Step 4: Re-run tests and iterate until green**
+
+### Task 3: Auth API routes
+
+**Files:**
+- Create: `apps/api/src/main.py`
+- Create: `apps/api/src/api/__init__.py`
+- Create: `apps/api/src/api/auth.py`
+- Test: `apps/api/tests/test_auth_api.py`
+
+- [ ] **Step 1: Write failing API tests for `/auth/login`, `/auth/callback`, `/auth/refresh`, `/auth/logout`, `/auth/me`**
+- [ ] **Step 2: Run targeted API tests to confirm red state**
+- [ ] **Step 3: Implement minimal route handlers and dependency wiring**
+- [ ] **Step 4: Re-run API tests until green**
+
+### Task 4: Verification and OpenSpec updates
+
+**Files:**
+- Modify: `openspec/changes/auth-oauth/tasks.md`
+
+- [ ] **Step 1: Run full checks: `pytest`, `ruff check src tests`, `mypy src`**
+- [ ] **Step 2: Run migration against local Postgres and verify current revision**
+- [ ] **Step 3: Mark completed checkboxes and capture any blockers in OpenSpec tasks**
diff --git a/docs/superpowers/plans/2026-05-17-database-models.md b/docs/superpowers/plans/2026-05-17-database-models.md
new file mode 100644
index 0000000..7821f02
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-17-database-models.md
@@ -0,0 +1,79 @@
+# Database Models Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build the backend database foundation for Headquarter with SQLAlchemy 2.0 async models, Alembic migrations, and development seed data.
+
+**Architecture:** Add a minimal FastAPI backend package under `apps/api/src` with one shared declarative base, one async database/session module, and focused model modules for each core entity. Drive the work from tests that assert schema metadata and relationship wiring first, then add Alembic and seeding on top.
+
+**Tech Stack:** Python 3.11, SQLAlchemy 2.x, asyncpg, Alembic, pytest, pytest-asyncio, Pydantic Settings, PostgreSQL JSONB/UUID types
+
+---
+
+### Task 1: Backend package skeleton and settings
+
+**Files:**
+- Create: `apps/api/src/__init__.py`
+- Create: `apps/api/src/config.py`
+- Create: `apps/api/src/database.py`
+- Test: `apps/api/tests/test_config.py`
+
+- [ ] Step 1: Write a failing test for configuration defaults and async engine URL expectations.
+- [ ] Step 2: Run the focused config test and confirm it fails because the module does not exist.
+- [ ] Step 3: Add minimal settings and async session factory implementation.
+- [ ] Step 4: Run the focused config test and confirm it passes.
+
+### Task 2: Declarative base and shared timestamp/UUID columns
+
+**Files:**
+- Create: `apps/api/src/models/__init__.py`
+- Create: `apps/api/src/models/base.py`
+- Test: `apps/api/tests/test_models.py`
+
+- [ ] Step 1: Write a failing metadata test that imports the base and asserts mapped tables can inherit UUID/timestamp columns.
+- [ ] Step 2: Run the focused model test and confirm it fails.
+- [ ] Step 3: Implement the declarative base plus reusable UUID/timestamp mixins.
+- [ ] Step 4: Re-run the focused model test and confirm it passes.
+
+### Task 3: Core entity models and relationships
+
+**Files:**
+- Create: `apps/api/src/models/user.py`
+- Create: `apps/api/src/models/project.py`
+- Create: `apps/api/src/models/git_repository.py`
+- Create: `apps/api/src/models/ssh_key.py`
+- Create: `apps/api/src/models/user_config.py`
+- Modify: `apps/api/src/models/__init__.py`
+- Test: `apps/api/tests/test_models.py`
+
+- [ ] Step 1: Write failing tests that assert the five tables exist, required columns are present, and the expected relationships are wired.
+- [ ] Step 2: Run the focused model tests and confirm they fail because the models are missing.
+- [ ] Step 3: Implement the minimal models to satisfy the spec, including PostgreSQL UUID/JSONB fields and foreign keys.
+- [ ] Step 4: Re-run the focused model tests and confirm they pass.
+
+### Task 4: Alembic integration and initial migration
+
+**Files:**
+- Create: `apps/api/alembic.ini`
+- Create: `apps/api/alembic/env.py`
+- Create: `apps/api/alembic/script.py.mako`
+- Create: `apps/api/alembic/versions/0001_initial_schema.py`
+- Test: `apps/api/tests/test_migration_metadata.py`
+
+- [ ] Step 1: Write a failing test that imports model metadata and asserts the initial migration covers all expected tables.
+- [ ] Step 2: Run the focused migration test and confirm it fails.
+- [ ] Step 3: Add minimal Alembic configuration plus an initial migration that creates all core tables.
+- [ ] Step 4: Re-run the focused migration test and confirm it passes.
+
+### Task 5: Seed data and verification
+
+**Files:**
+- Create: `apps/api/scripts/seed.py`
+- Modify: `openspec/changes/database-models/tasks.md`
+- Test: `apps/api/tests/test_seed.py`
+
+- [ ] Step 1: Write a failing test that verifies the seed module builds a deterministic development user payload.
+- [ ] Step 2: Run the focused seed test and confirm it fails.
+- [ ] Step 3: Implement the minimal seed helpers and script entrypoint.
+- [ ] Step 4: Re-run the focused seed test and confirm it passes.
+- [ ] Step 5: Mark completed OpenSpec checklist items and run the targeted verification commands.
diff --git a/docs/superpowers/specs/2026-05-17-auth-oauth-design.md b/docs/superpowers/specs/2026-05-17-auth-oauth-design.md
new file mode 100644
index 0000000..6141a05
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-17-auth-oauth-design.md
@@ -0,0 +1,170 @@
+# Auth OAuth Design
+
+## Goal
+
+Implement OAuth2/OIDC authentication via Authentik with internal JWT access tokens, DB-backed refresh token rotation, secure cookie handling, and explicit logout revocation.
+
+## Scope
+
+In scope:
+- Authentik code flow callback handling
+- Authentik token verification via JWKS
+- Internal JWT minting and validation
+- Refresh token persistence and rotation
+- Logout revocation and cookie clearing
+- Cookie policy split for dev vs production
+
+Out of scope:
+- RBAC policy engine
+- Multi-device session management UI
+- Social providers beyond existing Authentik setup
+
+## Chosen Approach
+
+Use full OIDC callback exchange with JWKS verification, then mint an internal short-lived JWT and store opaque refresh tokens server-side.
+
+Why this approach:
+- Keeps trust boundary explicit (no blind trust of exchange payload)
+- Allows immediate refresh-token revocation on logout
+- Decouples internal auth contract from external provider claim shape
+
+## Defaults
+
+- Access token TTL: 15 minutes
+- Refresh token TTL: 7 days
+- Cookie mode:
+ - Production: `Secure=true`, `SameSite=strict`, `httpOnly=true`
+ - Local development: `Secure=false`, `SameSite=lax`, `httpOnly=true`
+
+## Architecture
+
+1. Frontend calls `GET /auth/login`.
+2. Backend redirects to Authentik authorize endpoint.
+3. Authentik redirects to backend callback with `code`.
+4. Backend exchanges `code` for Authentik tokens.
+5. Backend validates Authentik access token using Authentik JWKS (`iss`, `aud`, `exp`, signature).
+6. Backend maps claims to local user record (create/update by `authentik_id`).
+7. Backend mints internal access JWT and opaque refresh token.
+8. Backend stores hashed refresh token in database and sets cookies.
+9. Protected endpoints validate internal access JWT.
+10. `POST /auth/refresh` rotates refresh token and issues new access JWT.
+11. `POST /auth/logout` revokes refresh token and clears cookies.
+
+## Data Model
+
+Add a `refresh_tokens` table:
+
+- `id`: UUID primary key
+- `user_id`: UUID foreign key -> `users.id`
+- `token_hash`: string (hash of opaque refresh token; never store raw token)
+- `expires_at`: timestamp with timezone
+- `created_at`: timestamp with timezone
+- `revoked_at`: timestamp with timezone, nullable
+- `user_agent`: string, nullable
+- `ip_address`: string, nullable
+
+Indexes:
+- unique index on `token_hash`
+- index on `user_id`
+- index on `expires_at`
+
+## API Endpoints
+
+### `GET /auth/login`
+
+- Redirects to Authentik authorize URL with state and nonce.
+
+### `GET /auth/callback`
+
+- Validates state.
+- Exchanges code at Authentik token endpoint.
+- Verifies Authentik access token via JWKS.
+- Upserts local user.
+- Mints internal access JWT + opaque refresh token.
+- Persists hashed refresh token record.
+- Sets `access_token` and `refresh_token` cookies.
+
+### `POST /auth/refresh`
+
+- Reads `refresh_token` cookie.
+- Hashes and finds matching non-revoked, non-expired DB row.
+- If valid, revokes old row and creates a new row (rotation).
+- Mints new internal access JWT and new opaque refresh token.
+- Sets rotated cookies.
+
+### `POST /auth/logout`
+
+- Reads refresh cookie if present.
+- Revokes corresponding DB token row.
+- Clears access and refresh cookies.
+
+### `GET /auth/me`
+
+- Validates internal access JWT.
+- Returns current user payload.
+
+## Token Strategy
+
+### Internal access JWT
+
+Claims:
+- `sub`: local user id
+- `email`
+- `name`
+- `roles` (optional, if available)
+- `iat`, `exp`
+
+Signing:
+- Use configured backend signing secret/algorithm.
+
+### Refresh token
+
+- Opaque, random, high-entropy value
+- Hashed before persistence
+- Rotated on each refresh
+- Revoked on logout and on detected reuse
+
+## Security Rules
+
+- Never expose token contents to frontend JS (httpOnly cookies only).
+- Validate Authentik token signature and critical claims before minting local JWT.
+- Enforce strict cookie attributes by environment.
+- Log security-relevant events with safe redaction.
+- Return generic auth errors to clients; keep details in server logs.
+
+## Error Handling
+
+- Invalid code exchange -> `401`
+- JWKS verification failure -> `401`
+- Missing/invalid refresh cookie -> `401`
+- Revoked/expired refresh token -> `401`
+- Reuse detection (if token already rotated/revoked) -> revoke chain and force login
+
+Error body shape:
+- stable machine-readable code
+- non-sensitive message
+
+## Testing Strategy
+
+Unit tests:
+- cookie option builder (dev vs prod)
+- Authentik token verification helper
+- internal JWT mint/verify helpers
+- refresh hash + rotation logic
+
+Integration tests:
+- callback creates/updates user and sets cookies
+- refresh rotates token and invalidates previous token
+- logout revokes token and clears cookies
+- protected endpoint rejects invalid/expired JWT
+
+Quality gates:
+- `pytest` passes
+- `mypy .` passes
+- `ruff check .` passes
+
+## Implementation Notes
+
+- Keep auth logic in focused modules (provider client, jwt service, refresh store, route handlers).
+- Keep DB writes idempotent where feasible (user upsert path).
+- Keep changes scoped to auth-oauth and required schema support.
diff --git a/openspec/changes/archive/2026-05-17-auth-oauth/.openspec.yaml b/openspec/changes/archive/2026-05-17-auth-oauth/.openspec.yaml
new file mode 100644
index 0000000..66da1ae
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-auth-oauth/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-05-17
diff --git a/openspec/changes/archive/2026-05-17-auth-oauth/README.md b/openspec/changes/archive/2026-05-17-auth-oauth/README.md
new file mode 100644
index 0000000..3200033
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-auth-oauth/README.md
@@ -0,0 +1,3 @@
+# auth-oauth
+
+Implement OAuth2/OIDC authentication via Authentik with internal JWTs and DB-backed refresh tokens
diff --git a/openspec/changes/archive/2026-05-17-auth-oauth/design.md b/openspec/changes/archive/2026-05-17-auth-oauth/design.md
new file mode 100644
index 0000000..63026e9
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-auth-oauth/design.md
@@ -0,0 +1,68 @@
+## Context
+
+The existing repository now includes core database infrastructure and user models, but authentication is not implemented yet. The target behavior is defined by `openspec/specs/auth-oauth/spec.md` and refined through approved decisions: Authentik OIDC as the identity provider, strict production cookies, internal JWT access tokens, and server-side refresh token storage with revocation.
+
+The backend is Python-based with async SQLAlchemy and Alembic. This change must integrate with that stack while keeping trust boundaries explicit and supporting predictable local development.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Implement Authentik OAuth2/OIDC login and callback flow.
+- Validate provider tokens via JWKS before creating local session credentials.
+- Mint internal short-lived JWT access tokens for API authorization.
+- Persist hashed opaque refresh tokens in DB with rotation and revocation.
+- Provide explicit logout that invalidates refresh state and clears cookies.
+- Enforce environment-aware cookie policy (strict in production, relaxed on localhost).
+
+**Non-Goals:**
+- RBAC policy engine and permission modeling.
+- Multi-device session management UX.
+- Additional social identity providers.
+
+## Decisions
+
+1. **Use OIDC code flow + Authentik JWKS validation before local minting**
+ - Rationale: prevents blind trust in token exchange payloads and centralizes signature/claim checks (`iss`, `aud`, `exp`).
+ - Alternative considered: trust exchange response without independent validation. Rejected due to weaker security posture.
+
+2. **Issue internal JWT access tokens instead of forwarding provider access tokens**
+ - Rationale: stable internal contract, decoupled claim shape, simpler downstream authorization.
+ - Alternative considered: pass-through provider tokens. Rejected due to tighter coupling and reduced control over TTL/claims.
+
+3. **Use DB-backed opaque refresh tokens with hash-at-rest + rotation**
+ - Rationale: supports immediate revocation on logout and tighter reuse detection.
+ - Alternative considered: stateless long-lived JWT refresh tokens. Rejected because revocation and replay handling are weaker.
+
+4. **Environment-aware cookie policy with secure defaults**
+ - Production: `Secure=true`, `SameSite=strict`, `HttpOnly=true`.
+ - Local dev: `Secure=false`, `SameSite=lax`, `HttpOnly=true`.
+ - Rationale: preserves security in production while enabling localhost development without TLS.
+
+5. **Add dedicated refresh token persistence model and migration**
+ - Table fields: `id`, `user_id`, `token_hash`, `expires_at`, `created_at`, `revoked_at`, `user_agent`, `ip_address`.
+ - Indexes: unique `token_hash`, plus `user_id` and `expires_at` indexes.
+
+## Risks / Trade-offs
+
+- **[JWKS endpoint/network failures]** -> Cache JWKS keys with bounded TTL and fail closed with `401` on unverifiable tokens.
+- **[Clock skew affecting token validity]** -> Allow small validation leeway and keep server clock synchronized.
+- **[Refresh token replay attempts]** -> Rotate per refresh, revoke reused chains, and force re-authentication.
+- **[Cookie behavior differences across browsers/environments]** -> Centralize cookie option builder and test dev/prod permutations.
+- **[Added implementation surface area]** -> Keep modules focused (provider client, JWT service, refresh store, routes) and maintain high test coverage.
+
+## Migration Plan
+
+1. Add new refresh token model and Alembic migration.
+2. Add configuration for OIDC endpoints/client credentials/JWT secret/cookie mode.
+3. Implement auth services (provider exchange + JWKS verification, JWT mint/verify, refresh store).
+4. Implement routes: `/auth/login`, `/auth/callback`, `/auth/refresh`, `/auth/logout`, `/auth/me`.
+5. Add/expand tests (unit + integration) for auth and token lifecycle.
+6. Verify quality gates (`pytest`, `ruff`, `mypy`) and migration status.
+
+Rollback:
+- Revert route/service changes and run Alembic downgrade for refresh-token migration if deployment requires rollback.
+
+## Open Questions
+
+- None blocking for initial implementation.
+- Optional follow-up: enforce single active refresh token per user-agent/device (currently out of scope).
diff --git a/openspec/changes/archive/2026-05-17-auth-oauth/proposal.md b/openspec/changes/archive/2026-05-17-auth-oauth/proposal.md
new file mode 100644
index 0000000..516ea78
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-auth-oauth/proposal.md
@@ -0,0 +1,27 @@
+## Why
+
+The project has database foundations in place but still lacks production-ready user authentication. We need a secure OAuth2/OIDC integration with Authentik that issues internal session credentials and supports reliable logout and token revocation.
+
+## What Changes
+
+- Implement backend OAuth2/OIDC login and callback flow against Authentik.
+- Verify Authentik-issued tokens via JWKS before minting local credentials.
+- Mint short-lived internal JWT access tokens and store refresh tokens server-side.
+- Add refresh-token rotation, revocation, and explicit logout semantics.
+- Apply environment-aware secure cookie policy (strict in production, relaxed for localhost development).
+- Add auth endpoints, supporting services, and test coverage for auth flows.
+
+## Capabilities
+
+### New Capabilities
+- `auth-session-tokens`: Internal JWT access-token issuance, opaque refresh-token storage, rotation, and revocation.
+
+### Modified Capabilities
+- `auth-oauth`: Extend OAuth/OIDC behavior to require JWKS validation, internal JWT minting, cookie policy by environment, and DB-backed refresh lifecycle.
+
+## Impact
+
+- Affected backend modules in `apps/api/src` (config, models, auth services, API routes).
+- New database schema object for refresh-token persistence and an accompanying migration.
+- New environment variables for OIDC/JWT/cookie settings.
+- Frontend auth integration points for login/logout/me/refresh behavior.
diff --git a/openspec/changes/archive/2026-05-17-auth-oauth/specs/auth-oauth/spec.md b/openspec/changes/archive/2026-05-17-auth-oauth/specs/auth-oauth/spec.md
new file mode 100644
index 0000000..32f9a6c
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-auth-oauth/specs/auth-oauth/spec.md
@@ -0,0 +1,62 @@
+## MODIFIED Requirements
+
+### Requirement: OAuth2/OIDC Flow
+The system SHALL support OAuth2/OIDC authentication via Authentik and SHALL validate Authentik-issued tokens via JWKS before creating local sessions.
+
+#### Scenario: User login
+- GIVEN a user clicks the login button
+- WHEN the frontend redirects to Authentik authorization endpoint
+- THEN the user authenticates with Authentik
+- AND Authentik redirects back with authorization code
+
+#### Scenario: Token exchange and validation
+- GIVEN Authentik has redirected with authorization code
+- WHEN the callback endpoint receives the code
+- THEN it exchanges the code for provider tokens
+- AND verifies token signature and claims using Authentik JWKS
+- AND upserts the local user account
+- AND mints internal access and refresh tokens
+
+### Requirement: Session Security
+The system SHALL protect sessions using httpOnly cookies and SHALL apply secure cookie defaults by environment.
+
+#### Scenario: Cookie attributes in production
+- GIVEN successful authentication in production
+- WHEN cookies are set
+- THEN access_token cookie SHALL be httpOnly
+- AND access_token cookie SHALL have Secure flag
+- AND access_token cookie SHALL have SameSite=strict
+- AND refresh_token cookie SHALL have the same attributes
+
+#### Scenario: Cookie attributes in localhost development
+- GIVEN successful authentication in localhost development
+- WHEN cookies are set
+- THEN access_token cookie SHALL be httpOnly
+- AND access_token cookie SHALL have Secure=false
+- AND access_token cookie SHALL have SameSite=lax
+- AND refresh_token cookie SHALL have the same attributes
+
+### Requirement: Token Refresh
+The system SHALL support automatic token refresh with server-side refresh token storage, rotation, and revocation.
+
+#### Scenario: Access token expiration
+- GIVEN a user has an expired access token
+- WHEN the user makes an authenticated request that can refresh
+- THEN the system validates the refresh token against non-expired, non-revoked DB state
+- AND rotates the refresh token
+- AND issues a new internal access token
+
+#### Scenario: Refresh token reuse detection
+- GIVEN a refresh token has already been rotated or revoked
+- WHEN it is presented again to the refresh endpoint
+- THEN the system rejects the request with unauthorized status
+- AND invalidates the token chain for the session
+
+### Requirement: Session Termination
+The system SHALL support explicit logout with refresh token invalidation.
+
+#### Scenario: User logout
+- GIVEN an authenticated user
+- WHEN the user clicks logout
+- THEN all auth cookies are cleared
+- AND the refresh token is invalidated in server-side storage
diff --git a/openspec/changes/archive/2026-05-17-auth-oauth/tasks.md b/openspec/changes/archive/2026-05-17-auth-oauth/tasks.md
new file mode 100644
index 0000000..a902417
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-auth-oauth/tasks.md
@@ -0,0 +1,38 @@
+## 1. Configuration and schema foundation
+
+- [x] 1.1 Add auth/OIDC/JWT/cookie settings to backend config with environment-aware defaults.
+- [x] 1.2 Add refresh token SQLAlchemy model and relationships to user model.
+- [x] 1.3 Add Alembic migration for refresh token table and indexes.
+- [x] 1.4 Add/adjust tests that fail first for config and refresh-token model metadata.
+
+## 2. Auth provider and token services
+
+- [x] 2.1 Implement Authentik OIDC client helpers for login URL build and callback token exchange.
+- [x] 2.2 Implement JWKS-based token verification helper for provider tokens.
+- [x] 2.3 Implement internal JWT mint/verify helper with configured TTL.
+- [x] 2.4 Implement refresh token store service (hashing, create, rotate, revoke, reuse detection).
+- [x] 2.5 Add unit tests for provider verification, JWT helpers, cookie options, and refresh lifecycle.
+
+## 3. Auth HTTP endpoints
+
+- [x] 3.1 Implement `GET /auth/login` redirect endpoint.
+- [x] 3.2 Implement `GET /auth/callback` with state validation, token exchange, user upsert, and cookie set.
+- [x] 3.3 Implement `POST /auth/refresh` with DB validation and rotation.
+- [x] 3.4 Implement `POST /auth/logout` to revoke refresh state and clear cookies.
+- [x] 3.5 Implement `GET /auth/me` returning authenticated user payload via internal JWT.
+- [x] 3.6 Add integration tests for callback, refresh rotation, logout, and unauthorized cases.
+
+## 4. Verification and OpenSpec tracking
+
+- [x] 4.1 Run auth-focused and full backend checks (`pytest`, `ruff check`, `mypy`) and fix findings.
+- [x] 4.2 Run migration verification against local Postgres and confirm current revision.
+- [x] 4.3 Update this task list with completed checkboxes and note any blockers/follow-ups.
+
+## Blockers / Follow-ups
+
+- No blocking items remain for this change.
+
+## Runtime verification
+
+- `DATABASE_URL=postgresql+asyncpg://headquarter:headquarter@localhost:5432/headquarter .venv/bin/alembic -c alembic.ini upgrade head` succeeded.
+- `DATABASE_URL=postgresql+asyncpg://headquarter:headquarter@localhost:5432/headquarter .venv/bin/alembic -c alembic.ini current` returned `0002_refresh_tokens (head)`.
diff --git a/openspec/changes/archive/2026-05-17-database-models/.openspec.yaml b/openspec/changes/archive/2026-05-17-database-models/.openspec.yaml
new file mode 100644
index 0000000..66da1ae
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-database-models/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-05-17
diff --git a/openspec/changes/archive/2026-05-17-database-models/README.md b/openspec/changes/archive/2026-05-17-database-models/README.md
new file mode 100644
index 0000000..093bfc5
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-database-models/README.md
@@ -0,0 +1,3 @@
+# database-models
+
+Implement SQLAlchemy models and Alembic migrations for all core entities
diff --git a/openspec/changes/archive/2026-05-17-database-models/design.md b/openspec/changes/archive/2026-05-17-database-models/design.md
new file mode 100644
index 0000000..f2d2038
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-database-models/design.md
@@ -0,0 +1,125 @@
+# Design: Database Models
+
+## Technology Choices
+
+- **SQLAlchemy 2.0**: Modern async ORM with type annotations
+- **asyncpg**: High-performance async PostgreSQL driver
+- **Alembic**: Database migration tool
+- **UUID**: All primary keys use UUID for distributed safety
+
+## Architecture
+
+### Base Model
+
+All models inherit from a common base with:
+- `id`: UUID primary key (default=uuid4)
+- `created_at`: Timestamp
+- `updated_at`: Timestamp (auto-updated)
+
+### Models
+
+1. **User**
+ - id: UUID PK
+ - email: str, unique, indexed
+ - name: str
+ - authentik_id: str, unique (external auth reference)
+ - avatar_url: str | None
+ - created_at, updated_at
+
+2. **Project**
+ - id: UUID PK
+ - name: str
+ - description: str | None
+ - owner_id: UUID → User
+ - default_ssh_key_id: UUID → SSHKey | None
+ - created_at, updated_at
+
+3. **GitRepository**
+ - id: UUID PK
+ - name: str
+ - path: str (filesystem path to bare repo)
+ - project_id: UUID → Project
+ - owner_id: UUID → User
+ - is_mirror: bool
+ - remote_url: str | None
+ - last_push: datetime | None
+ - created_at
+
+4. **SSHKey**
+ - id: UUID PK
+ - name: str
+ - public_key: str
+ - private_key_encrypted: str (Fernet encrypted)
+ - user_id: UUID → User
+ - project_id: UUID → Project | None
+ - created_at
+
+5. **UserConfig**
+ - id: UUID PK
+ - user_id: UUID → User
+ - config: JSONB (PostgreSQL native JSON)
+ - created_at, updated_at
+
+### Relationships
+
+```
+User 1--N Project
+User 1--N SSHKey
+User 1--1 UserConfig
+Project 1--N GitRepository
+Project N--1 SSHKey (default_ssh_key)
+```
+
+### File Structure
+
+```
+apps/api/
+├── src/
+│ ├── models/
+│ │ ├── __init__.py
+│ │ ├── base.py # DeclarativeBase + common columns
+│ │ ├── user.py
+│ │ ├── project.py
+│ │ ├── git_repository.py
+│ │ ├── ssh_key.py
+│ │ └── user_config.py
+│ ├── database.py # Async engine + session
+│ └── config.py # Settings with pydantic-settings
+├── alembic/
+│ ├── env.py
+│ ├── script.py.mako
+│ └── versions/
+│ └── 001_initial.py
+├── tests/
+│ └── test_models.py
+└── scripts/
+ └── seed.py
+```
+
+## Async Pattern
+
+```python
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
+from sqlalchemy.orm import sessionmaker
+
+engine = create_async_engine(DATABASE_URL)
+AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession)
+```
+
+## Migration Strategy
+
+- Single initial migration creating all tables
+- Future migrations use `alembic revision --autogenerate`
+- Run with `make migrate` (docker compose exec api alembic upgrade head)
+
+## Seed Data
+
+- Create a test user with sample data
+- Run via `docker compose exec api python scripts/seed.py`
+
+## Quality Gates
+
+- pytest with async test support
+- mypy strict mode
+- ruff for linting
+- All models have type annotations
diff --git a/openspec/changes/archive/2026-05-17-database-models/proposal.md b/openspec/changes/archive/2026-05-17-database-models/proposal.md
new file mode 100644
index 0000000..1bad2a1
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-database-models/proposal.md
@@ -0,0 +1,28 @@
+# Proposal: Database Models
+
+## What
+
+Implement SQLAlchemy 2.0 async models and Alembic migrations for all core entities in the Headquarter platform.
+
+## Why
+
+All other features (auth, git repos, projects, SSH keys, etc.) depend on a solid database foundation. We need models that:
+- Use SQLAlchemy 2.0 async style for performance
+- Support all entity relationships defined in the specs
+- Have proper migrations for schema versioning
+- Include seed data for development
+
+## Scope
+
+- User, Project, GitRepository, SSHKey, UserConfig models
+- Alembic setup with asyncpg support
+- Initial migration creating all tables
+- Database seeding script
+
+## Success Criteria
+
+- All models defined with correct relationships
+- Initial migration runs successfully
+- `make migrate` works
+- Seed script creates test data
+- Quality gates pass (pytest, mypy, ruff)
diff --git a/openspec/changes/archive/2026-05-17-database-models/tasks.md b/openspec/changes/archive/2026-05-17-database-models/tasks.md
new file mode 100644
index 0000000..adf8510
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-database-models/tasks.md
@@ -0,0 +1,64 @@
+# Tasks: Database Models
+
+## Task 1: Create project structure and configuration
+- [x] Create `apps/api/src/models/__init__.py`
+- [x] Create `apps/api/src/config.py` with pydantic-settings for database URL
+- [x] Create `apps/api/src/database.py` with async engine and session
+
+## Task 2: Create base model
+- [x] Create `apps/api/src/models/base.py` with DeclarativeBase
+- [x] Add UUID primary key mixin
+- [x] Add timestamp mixin (created_at, updated_at)
+
+## Task 3: Create User model
+- [x] Create `apps/api/src/models/user.py`
+- [x] Define User with all fields from spec
+- [x] Add relationships to Project, SSHKey, UserConfig
+
+## Task 4: Create Project model
+- [x] Create `apps/api/src/models/project.py`
+- [x] Define Project with all fields
+- [x] Add relationships to User, GitRepository, SSHKey
+
+## Task 5: Create GitRepository model
+- [x] Create `apps/api/src/models/git_repository.py`
+- [x] Define GitRepository with all fields
+- [x] Add relationships to Project, User
+
+## Task 6: Create SSHKey model
+- [x] Create `apps/api/src/models/ssh_key.py`
+- [x] Define SSHKey with all fields
+- [x] Add relationships to User, Project
+
+## Task 7: Create UserConfig model
+- [x] Create `apps/api/src/models/user_config.py`
+- [x] Define UserConfig with JSONB config field
+- [x] Add relationship to User
+
+## Task 8: Initialize Alembic
+- [x] Create Alembic scaffolding equivalent to `alembic init`
+- [x] Configure `alembic/env.py` for async
+- [x] Update `alembic.ini` with correct URL
+
+## Task 9: Create initial migration
+- [x] Generate migration creating all tables
+- [x] Verify migration is correct
+
+## Task 10: Create seed script
+- [x] Create `apps/api/src/scripts/seed.py`
+- [x] Add test user and sample data payload helper
+- [x] Make script runnable
+
+## Task 11: Create tests
+- [x] Create `apps/api/tests/test_models.py`
+- [x] Test model creation and relationships
+- [x] Test async database operations
+
+## Task 12: Run quality gates
+- [x] Run `pytest` - all tests pass
+- [x] Run `mypy .` - no type errors
+- [x] Run `ruff check .` - no lint errors
+
+## Runtime Verification
+
+- [x] Run migrations against a live PostgreSQL instance to verify end-to-end database execution.
diff --git a/openspec/changes/archive/2026-05-17-frontend-foundation/.openspec.yaml b/openspec/changes/archive/2026-05-17-frontend-foundation/.openspec.yaml
new file mode 100644
index 0000000..66da1ae
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-frontend-foundation/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-05-17
diff --git a/openspec/changes/archive/2026-05-17-frontend-foundation/README.md b/openspec/changes/archive/2026-05-17-frontend-foundation/README.md
new file mode 100644
index 0000000..ede413b
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-frontend-foundation/README.md
@@ -0,0 +1,3 @@
+# frontend-foundation
+
+Implement frontend app foundation with auth-aware shell, routing skeleton, and API integration base
diff --git a/openspec/changes/archive/2026-05-17-frontend-foundation/design.md b/openspec/changes/archive/2026-05-17-frontend-foundation/design.md
new file mode 100644
index 0000000..211a5dc
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-frontend-foundation/design.md
@@ -0,0 +1,56 @@
+## Context
+
+`apps/web` currently contains only package and container scaffolding, with no source code. Backend authentication and API foundations are now available, including cookie-based auth flows. The frontend foundation must establish a maintainable structure that supports authenticated navigation, responsive layout behavior, and consistent API communication.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Create a minimal but production-oriented React app structure with TypeScript and Vite.
+- Add client-side routing with protected-route behavior and fallback 404 route.
+- Provide an authenticated shell layout with desktop sidebar and mobile navigation affordances.
+- Add a shared API client that sends credentials and handles unauthorized responses.
+- Provide a starter dashboard page with loading and error-safe patterns.
+
+**Non-Goals:**
+- Full feature implementation for projects/repositories/ssh keys/settings pages.
+- Pixel-perfect final design system and component library.
+- Advanced state-management framework adoption beyond required foundation.
+
+## Decisions
+
+1. **Route-centric app composition with `react-router-dom` data boundaries kept simple**
+ - Rationale: aligns with existing dependency set and keeps first milestone small.
+ - Alternative: add heavier route/data framework patterns now. Rejected as unnecessary for foundation stage.
+
+2. **Auth state bootstraps from `/auth/me` and routes guard against missing session**
+ - Rationale: backend is source of truth for cookie-backed identity; avoids duplicative token logic in browser.
+ - Alternative: local token/session storage. Rejected for weaker security and mismatch with cookie strategy.
+
+3. **Single API client module wrapping Axios defaults and 401 interception**
+ - Rationale: centralizes credential behavior and unauthorized handling.
+ - Alternative: per-request fetch wrappers across pages. Rejected due to duplication risk.
+
+4. **App shell-first approach before deep page content**
+ - Rationale: navigation and responsive structure are prerequisites for all future feature pages.
+ - Alternative: implement pages first then refactor into shell. Rejected due to avoidable churn.
+
+## Risks / Trade-offs
+
+- **[Auth bootstrap flicker on first load]** -> use explicit loading screen until session check resolves.
+- **[401 redirect loops]** -> add interceptor guard and avoid redirecting when already on public/auth routes.
+- **[Responsive nav complexity early]** -> keep mobile behavior minimal (toggleable drawer) and iterate later.
+- **[Frontend/backend contract drift]** -> codify expected endpoint behavior in integration-oriented frontend tests.
+
+## Migration Plan
+
+1. Scaffold source tree (`main.tsx`, app router, shell, pages, API client, styles).
+2. Implement auth context + protected route guard and login/logout wiring.
+3. Implement responsive shell and dashboard placeholder content.
+4. Add checks/tests and run `npm run typecheck`, `npm run lint`, `npm run build`.
+
+Rollback:
+- Remove added source tree and revert package/config changes if foundation rollout is paused.
+
+## Open Questions
+
+- Whether to include React Query in the next frontend increment (deferred; not required for foundation).
diff --git a/openspec/changes/archive/2026-05-17-frontend-foundation/proposal.md b/openspec/changes/archive/2026-05-17-frontend-foundation/proposal.md
new file mode 100644
index 0000000..b96c2fe
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-frontend-foundation/proposal.md
@@ -0,0 +1,26 @@
+## Why
+
+The project has backend foundations and authentication flows, but the frontend currently has no application code to consume them. We need a usable React foundation so users can authenticate, navigate core areas, and interact with APIs consistently across desktop and mobile.
+
+## What Changes
+
+- Establish the initial React + TypeScript app structure in `apps/web` with Vite conventions.
+- Add routing skeleton with protected routes, not-found handling, and auth-aware redirects.
+- Implement a baseline app shell (header, sidebar/mobile nav, content area) for authenticated screens.
+- Add shared API client configuration for cookie-based auth and 401 handling.
+- Add initial dashboard scaffolding with loading/error states and placeholder summary cards.
+- Add frontend quality gates and tests/checks for routing/auth behaviors and build integrity.
+
+## Capabilities
+
+### New Capabilities
+- `frontend-auth-shell`: Auth-aware layout primitives and guarded route flow for the web app.
+
+### Modified Capabilities
+- `frontend-foundation`: Tighten requirements around route protection, API credential handling, and responsive authenticated shell behavior.
+
+## Impact
+
+- Affected app code under `apps/web` (new source tree, routes, layout, API client, styles).
+- Depends on backend auth endpoints (`/auth/login`, `/auth/logout`, `/auth/me`, `/auth/refresh`) for session flow.
+- Introduces frontend config conventions for API base URL and runtime auth assumptions.
diff --git a/openspec/changes/archive/2026-05-17-frontend-foundation/specs/frontend-foundation/spec.md b/openspec/changes/archive/2026-05-17-frontend-foundation/specs/frontend-foundation/spec.md
new file mode 100644
index 0000000..964d43c
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-frontend-foundation/specs/frontend-foundation/spec.md
@@ -0,0 +1,71 @@
+## MODIFIED Requirements
+
+### Requirement: React Application Setup
+The system SHALL use React 18+ with TypeScript and SHALL provide a runnable application source structure in `apps/web/src`.
+
+#### Scenario: Frontend build
+- GIVEN the frontend codebase
+- THEN it SHALL:
+ - Use React 18+ with TypeScript 5+
+ - Use Vite as the build tool
+ - Support Hot Module Replacement (HMR)
+ - Output optimized production builds
+ - Include a concrete entrypoint, app composition, and route tree
+
+### Requirement: Client-Side Routing
+The system SHALL implement client-side routing with authenticated route guards and explicit not-found handling.
+
+#### Scenario: Navigation
+- GIVEN the frontend application
+- THEN React Router SHALL:
+ - Define routes for all foundation pages
+ - Support protected routes (require authentication)
+ - Handle 404 errors
+ - Support route parameters for feature pages
+
+#### Scenario: Protected routes
+- GIVEN an unauthenticated user
+- WHEN they access a protected route
+- THEN they are redirected to login flow
+- AND post-auth navigation returns them to an authenticated landing route
+
+### Requirement: Layout Component
+The system SHALL provide a consistent application layout for authenticated screens across desktop and mobile sizes.
+
+#### Scenario: Application shell
+- GIVEN the frontend application
+- THEN a Layout component SHALL:
+ - Display a header with user info and logout
+ - Display sidebar navigation on desktop
+ - Show main content area
+ - Collapse sidebar into a mobile menu toggle on small viewports
+
+#### Scenario: Navigation links
+- GIVEN the sidebar navigation
+- THEN it SHALL include links to:
+ - Dashboard
+ - Projects
+ - Repositories
+ - SSH Keys
+ - Settings
+
+### Requirement: HTTP Client Configuration
+The system SHALL configure HTTP requests for cookie-based auth and unauthorized-session recovery.
+
+#### Scenario: API communication
+- GIVEN the frontend application
+- THEN Axios/fetch SHALL:
+ - Send credentials (cookies) with requests
+ - Handle 401 responses by redirecting to login
+ - Set appropriate content-type headers
+ - Support request/response interception in a shared client module
+
+### Requirement: Loading States
+The system SHALL handle asynchronous operations gracefully during auth bootstrap and dashboard fetches.
+
+#### Scenario: Data fetching
+- GIVEN a page loading data
+- THEN:
+ - Loading states are shown while requests are in flight
+ - Errors are shown with retry affordance
+ - Initial auth-check loading prevents protected-layout flicker
diff --git a/openspec/changes/archive/2026-05-17-frontend-foundation/tasks.md b/openspec/changes/archive/2026-05-17-frontend-foundation/tasks.md
new file mode 100644
index 0000000..47a9f65
--- /dev/null
+++ b/openspec/changes/archive/2026-05-17-frontend-foundation/tasks.md
@@ -0,0 +1,28 @@
+## 1. Frontend app scaffold and routing
+
+- [x] 1.1 Create `apps/web/src` app entrypoint, base styles, and root render wiring.
+- [x] 1.2 Add router configuration with dashboard, projects, repositories, ssh keys, settings, and not-found routes.
+- [x] 1.3 Add protected-route guard and login redirect behavior for unauthenticated access.
+
+## 2. Auth-aware shell and API client
+
+- [x] 2.1 Implement shared API client with credentialed requests and 401 handling strategy.
+- [x] 2.2 Implement auth session bootstrap (`/auth/me`) and lightweight auth context/provider.
+- [x] 2.3 Implement app shell layout (header, desktop sidebar, mobile menu toggle, content outlet).
+- [x] 2.4 Wire logout action to backend endpoint and session-state reset.
+
+## 3. Dashboard and UX states
+
+- [x] 3.1 Implement dashboard placeholder page with summary cards and quick actions.
+- [x] 3.2 Add loading, empty, and error-retry states for dashboard/auth bootstrap paths.
+- [x] 3.3 Ensure responsive behavior for mobile viewport navigation and touch targets.
+
+## 4. Verification and OpenSpec tracking
+
+- [x] 4.1 Add/update frontend tests for protected routing and auth/session behaviors.
+- [x] 4.2 Run frontend quality gates (`npm run typecheck`, `npm run lint`, `npm run build`) and fix findings.
+- [x] 4.3 Update this tasks file with completed checkboxes and note blockers/follow-ups.
+
+## Blockers / Follow-ups
+
+- None at this stage.
diff --git a/openspec/changes/project-management/.openspec.yaml b/openspec/changes/project-management/.openspec.yaml
new file mode 100644
index 0000000..66da1ae
--- /dev/null
+++ b/openspec/changes/project-management/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-05-17
diff --git a/openspec/changes/project-management/README.md b/openspec/changes/project-management/README.md
new file mode 100644
index 0000000..0912e93
--- /dev/null
+++ b/openspec/changes/project-management/README.md
@@ -0,0 +1,3 @@
+# project-management
+
+Implement project CRUD, listing, and ownership workflows across API and frontend
diff --git a/openspec/changes/project-management/design.md b/openspec/changes/project-management/design.md
new file mode 100644
index 0000000..02c9efe
--- /dev/null
+++ b/openspec/changes/project-management/design.md
@@ -0,0 +1,57 @@
+## Context
+
+Project management is defined in specs but not yet fully implemented across backend and frontend. The codebase now includes auth-oauth and frontend-foundation, so this change should connect authenticated users to project CRUD workflows and enforce ownership/security rules consistently.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Provide authenticated CRUD endpoints for projects with owner-only updates/deletes.
+- Expose project listing payloads that include associated repositories and default SSH key metadata.
+- Add frontend project pages for listing, creating, editing, and deleting projects.
+- Support setting/changing a default SSH key per project with ownership validation.
+- Verify cascade behavior and authorization through tests.
+
+**Non-Goals:**
+- Advanced collaboration features (shared project ownership, invites, roles).
+- Bulk project operations.
+- Full analytics/activity stream implementation beyond simple listing metadata.
+
+## Decisions
+
+1. **Keep project logic in dedicated service layer with thin route handlers**
+ - Rationale: consistent with auth service separation and easier unit testing.
+ - Alternative: embed logic directly in route handlers. Rejected for maintainability.
+
+2. **Use authenticated user identity from internal JWT for ownership checks**
+ - Rationale: single trust path and no client-provided owner fields.
+ - Alternative: allow owner IDs in request body. Rejected for security risk.
+
+3. **Return normalized project DTOs with nested repository summaries**
+ - Rationale: reduces frontend round-trips and supports immediate dashboard/project page rendering.
+ - Alternative: fetch repositories separately per project. Rejected due to extra request overhead.
+
+4. **Implement optimistic-friendly frontend forms with explicit server error display**
+ - Rationale: better UX while preserving clear failure feedback.
+ - Alternative: full page reload after each action. Rejected due to poor interaction quality.
+
+## Risks / Trade-offs
+
+- **[Ownership bypass bugs]** -> enforce auth checks at service boundary and test unauthorized scenarios.
+- **[Cascade deletion surprises]** -> add integration tests that assert repository/association cleanup behavior.
+- **[Stale frontend lists after mutation]** -> centralize refresh calls after create/update/delete.
+- **[Default SSH key mismatch]** -> validate key belongs to user/project context before assignment.
+
+## Migration Plan
+
+1. Add/adjust backend project API routes and services.
+2. Add backend tests for CRUD, ownership, default-key, and cascade behavior.
+3. Implement frontend project pages and API integration.
+4. Add frontend tests for protected interactions and mutation flows.
+5. Run quality gates for backend and frontend.
+
+Rollback:
+- Revert project API and frontend pages; keep existing schema unchanged unless explicit migration is added.
+
+## Open Questions
+
+- Whether project descriptions should support markdown formatting (deferred; plain text for now).
diff --git a/openspec/changes/project-management/proposal.md b/openspec/changes/project-management/proposal.md
new file mode 100644
index 0000000..aa825c4
--- /dev/null
+++ b/openspec/changes/project-management/proposal.md
@@ -0,0 +1,27 @@
+## Why
+
+The platform now has authentication, data models, and a frontend foundation, but users still cannot manage projects end-to-end. Implementing project management is the next core workflow needed to organize repositories and execute real work.
+
+## What Changes
+
+- Implement authenticated project CRUD APIs with ownership enforcement.
+- Add project listing views and project create/update/delete flows in the frontend.
+- Add default SSH key selection per project and validate key ownership.
+- Enforce cascading behavior for project deletion with related repositories and SSH key links.
+- Add backend and frontend tests that cover auth, ownership, and error cases.
+
+## Capabilities
+
+### New Capabilities
+- `project-management-ui`: Frontend project list/detail/form flows integrated with authenticated API.
+
+### Modified Capabilities
+- `project-management`: Expand requirement details for ownership checks, API contracts, and frontend-integrated project workflows.
+- `git-repo`: Clarify cascade/delete behavior when repositories are removed through project deletion.
+- `ssh-keys`: Clarify constraints for default project SSH key assignment.
+
+## Impact
+
+- Backend changes in `apps/api/src` (routes/services/models validation) and API tests.
+- Frontend changes in `apps/web/src` (project pages, API client calls, forms, state handling).
+- Potential migration adjustments if additional indexes/constraints are required for ownership/default-key integrity.
diff --git a/openspec/changes/project-management/specs/git-repo/spec.md b/openspec/changes/project-management/specs/git-repo/spec.md
new file mode 100644
index 0000000..5489f96
--- /dev/null
+++ b/openspec/changes/project-management/specs/git-repo/spec.md
@@ -0,0 +1,10 @@
+## MODIFIED Requirements
+
+### Requirement: Repository Deletion
+The system SHALL remove repository records when their owning project is deleted through authorized project deletion flow.
+
+#### Scenario: Cascade repository cleanup
+- GIVEN a project with associated repositories
+- WHEN the project owner deletes the project
+- THEN repository records for that project are removed
+- AND repository listing no longer includes removed records
diff --git a/openspec/changes/project-management/specs/project-management/spec.md b/openspec/changes/project-management/specs/project-management/spec.md
new file mode 100644
index 0000000..0986312
--- /dev/null
+++ b/openspec/changes/project-management/specs/project-management/spec.md
@@ -0,0 +1,69 @@
+## MODIFIED Requirements
+
+### Requirement: Project Creation
+The system SHALL allow authenticated users to create new projects and SHALL assign the creator as project owner.
+
+#### Scenario: Create project
+- GIVEN an authenticated user
+- WHEN they create a project with name and description
+- THEN a project record is created
+- AND the user is set as owner
+
+#### Scenario: Reject unauthenticated creation
+- GIVEN a request without a valid authenticated session
+- WHEN it attempts to create a project
+- THEN the system responds with unauthorized status
+
+### Requirement: Project Listing
+The system SHALL list projects owned by the authenticated user, including related repositories and default SSH key metadata.
+
+#### Scenario: List projects
+- GIVEN an authenticated user
+- WHEN they view the projects page
+- THEN all their projects are listed with associated repositories
+
+#### Scenario: Ownership-scoped listing
+- GIVEN multiple users with separate projects
+- WHEN one user requests their project list
+- THEN only that user's projects are returned
+
+### Requirement: Project Updates
+The system SHALL support updating project details for project owners only.
+
+#### Scenario: Update project
+- GIVEN a project owner
+- WHEN they update the name or description
+- THEN the changes are persisted
+
+#### Scenario: Non-owner update denied
+- GIVEN a user who is not the project owner
+- WHEN they attempt to update project details
+- THEN the system responds with forbidden status
+
+### Requirement: Project Deletion
+The system SHALL support cascading project deletion for project owners.
+
+#### Scenario: Delete project
+- GIVEN a project owner
+- WHEN they delete a project
+- THEN all associated repositories are deleted
+- AND all associated SSH keys are removed
+- AND the project record is deleted
+
+#### Scenario: Non-owner deletion denied
+- GIVEN a user who is not the project owner
+- WHEN they attempt to delete the project
+- THEN the system responds with forbidden status
+
+### Requirement: Default SSH Key
+The system SHALL allow project owners to set a default SSH key per project and SHALL validate ownership for selected keys.
+
+#### Scenario: Set default key
+- GIVEN a project with SSH keys
+- WHEN the owner selects a default key
+- THEN it's used for git operations in that project
+
+#### Scenario: Reject foreign key assignment
+- GIVEN a project owner
+- WHEN they try setting a default SSH key that does not belong to their allowed scope
+- THEN the system rejects the request with validation error
diff --git a/openspec/changes/project-management/specs/ssh-keys/spec.md b/openspec/changes/project-management/specs/ssh-keys/spec.md
new file mode 100644
index 0000000..1646b4d
--- /dev/null
+++ b/openspec/changes/project-management/specs/ssh-keys/spec.md
@@ -0,0 +1,14 @@
+## MODIFIED Requirements
+
+### Requirement: SSH Key Project Association
+The system SHALL only allow project default-key assignment using keys valid for the authenticated owner's project scope.
+
+#### Scenario: Valid default key assignment
+- GIVEN a project owner and an eligible SSH key
+- WHEN the owner sets the key as default for the project
+- THEN the project stores that key reference
+
+#### Scenario: Invalid default key assignment
+- GIVEN a project owner
+- WHEN the owner attempts to set an ineligible SSH key as project default
+- THEN the system responds with validation failure
diff --git a/openspec/changes/project-management/tasks.md b/openspec/changes/project-management/tasks.md
new file mode 100644
index 0000000..060d8a5
--- /dev/null
+++ b/openspec/changes/project-management/tasks.md
@@ -0,0 +1,25 @@
+## 1. Backend project API and ownership enforcement
+
+- [x] 1.1 Add/extend project API routes for create, list, update, delete, and default-ssh-key set operations.
+- [x] 1.2 Implement project service layer ownership checks using authenticated user identity.
+- [x] 1.3 Implement/verify cascade semantics for project deletion with repository cleanup behavior.
+- [x] 1.4 Add backend tests for authenticated CRUD, ownership-denied scenarios, and default-key validation.
+
+## 2. Frontend project management flows
+
+- [x] 2.1 Add project API client methods for list/create/update/delete/default-key operations.
+- [x] 2.2 Implement projects page with list and empty/loading/error states.
+- [x] 2.3 Implement project create/edit form interactions and validation messaging.
+- [x] 2.4 Implement delete flow and UI refresh behavior after mutations.
+- [x] 2.5 Add frontend tests for protected project routes and mutation flows.
+
+## 3. Verification and OpenSpec tracking
+
+- [x] 3.1 Run backend checks (`pytest`, `ruff check src tests`, `mypy src`) and fix findings.
+- [x] 3.2 Run frontend checks (`npm test`, `npm run typecheck`, `npm run lint`, `npm run build`) and fix findings.
+- [x] 3.3 Update this tasks file with completed checkboxes and document blockers/follow-ups.
+
+## Blockers / Follow-ups
+
+- No blocking issues remain for this change.
+- Frontend test run shows non-blocking Vite deprecation warnings related to esbuild options from `vite:react-babel`; behavior is unaffected and can be handled in a later tooling cleanup change.
diff --git a/openspec/specs/auth-oauth/spec.md b/openspec/specs/auth-oauth/spec.md
index bb97361..77a5cd9 100644
--- a/openspec/specs/auth-oauth/spec.md
+++ b/openspec/specs/auth-oauth/spec.md
@@ -3,12 +3,9 @@
## Purpose
Manage user authentication via Authentik OAuth with secure session handling.
-
## Requirements
-
### Requirement: OAuth2/OIDC Flow
-
-The system SHALL support OAuth2/OIDC authentication via Authentik.
+The system SHALL support OAuth2/OIDC authentication via Authentik and SHALL validate Authentik-issued tokens via JWKS before creating local sessions.
#### Scenario: User login
- GIVEN a user clicks the login button
@@ -16,43 +13,57 @@ The system SHALL support OAuth2/OIDC authentication via Authentik.
- THEN the user authenticates with Authentik
- AND Authentik redirects back with authorization code
-#### Scenario: Token exchange
+#### Scenario: Token exchange and validation
- GIVEN Authentik has redirected with authorization code
- WHEN the callback endpoint receives the code
-- THEN it exchanges the code for access and refresh tokens
-- AND sets httpOnly, Secure, SameSite=strict cookies
+- THEN it exchanges the code for provider tokens
+- AND verifies token signature and claims using Authentik JWKS
+- AND upserts the local user account
+- AND mints internal access and refresh tokens
### Requirement: Session Security
+The system SHALL protect sessions using httpOnly cookies and SHALL apply secure cookie defaults by environment.
-The system SHALL protect sessions using httpOnly cookies.
-
-#### Scenario: Cookie attributes
-- GIVEN successful authentication
+#### Scenario: Cookie attributes in production
+- GIVEN successful authentication in production
- WHEN cookies are set
- THEN access_token cookie SHALL be httpOnly
- AND access_token cookie SHALL have Secure flag
- AND access_token cookie SHALL have SameSite=strict
-- AND refresh_token cookie SHALL have same attributes
+- AND refresh_token cookie SHALL have the same attributes
+
+#### Scenario: Cookie attributes in localhost development
+- GIVEN successful authentication in localhost development
+- WHEN cookies are set
+- THEN access_token cookie SHALL be httpOnly
+- AND access_token cookie SHALL have Secure=false
+- AND access_token cookie SHALL have SameSite=lax
+- AND refresh_token cookie SHALL have the same attributes
### Requirement: Token Refresh
-
-The system SHALL support automatic token refresh.
+The system SHALL support automatic token refresh with server-side refresh token storage, rotation, and revocation.
#### Scenario: Access token expiration
- GIVEN a user has an expired access token
-- WHEN the user makes an authenticated request
-- THEN the system uses the refresh token to get a new access token
+- WHEN the user makes an authenticated request that can refresh
+- THEN the system validates the refresh token against non-expired, non-revoked DB state
- AND rotates the refresh token
+- AND issues a new internal access token
+
+#### Scenario: Refresh token reuse detection
+- GIVEN a refresh token has already been rotated or revoked
+- WHEN it is presented again to the refresh endpoint
+- THEN the system rejects the request with unauthorized status
+- AND invalidates the token chain for the session
### Requirement: Session Termination
-
-The system SHALL support explicit logout.
+The system SHALL support explicit logout with refresh token invalidation.
#### Scenario: User logout
- GIVEN an authenticated user
- WHEN the user clicks logout
- THEN all auth cookies are cleared
-- AND the refresh token is invalidated
+- AND the refresh token is invalidated in server-side storage
## Dependencies
diff --git a/openspec/specs/frontend-foundation/spec.md b/openspec/specs/frontend-foundation/spec.md
index d7149ef..9c0f9fa 100644
--- a/openspec/specs/frontend-foundation/spec.md
+++ b/openspec/specs/frontend-foundation/spec.md
@@ -3,12 +3,9 @@
## Purpose
Provide a modern React frontend with TypeScript, routing, and responsive layout.
-
## Requirements
-
### Requirement: React Application Setup
-
-The system SHALL use React 18+ with TypeScript.
+The system SHALL use React 18+ with TypeScript and SHALL provide a runnable application source structure in `apps/web/src`.
#### Scenario: Frontend build
- GIVEN the frontend codebase
@@ -17,23 +14,24 @@ The system SHALL use React 18+ with TypeScript.
- Use Vite as the build tool
- Support Hot Module Replacement (HMR)
- Output optimized production builds
+ - Include a concrete entrypoint, app composition, and route tree
### Requirement: Client-Side Routing
-
-The system SHALL implement client-side routing.
+The system SHALL implement client-side routing with authenticated route guards and explicit not-found handling.
#### Scenario: Navigation
- GIVEN the frontend application
- THEN React Router SHALL:
- - Define routes for all pages
+ - Define routes for all foundation pages
- Support protected routes (require authentication)
- Handle 404 errors
- - Support route parameters
+ - Support route parameters for feature pages
#### Scenario: Protected routes
- GIVEN an unauthenticated user
- WHEN they access a protected route
-- THEN they are redirected to login
+- THEN they are redirected to login flow
+- AND post-auth navigation returns them to an authenticated landing route
### Requirement: Styling Framework
@@ -48,16 +46,15 @@ The system SHALL use Tailwind CSS for styling.
- Support dark mode
### Requirement: Layout Component
-
-The system SHALL provide a consistent application layout.
+The system SHALL provide a consistent application layout for authenticated screens across desktop and mobile sizes.
#### Scenario: Application shell
- GIVEN the frontend application
- THEN a Layout component SHALL:
- Display a header with user info and logout
- - Display a sidebar with navigation links
+ - Display sidebar navigation on desktop
- Show main content area
- - Collapse sidebar on mobile
+ - Collapse sidebar into a mobile menu toggle on small viewports
#### Scenario: Navigation links
- GIVEN the sidebar navigation
@@ -81,19 +78,17 @@ The system SHALL support mobile devices.
- Touch targets are appropriately sized
### Requirement: Loading States
-
-The system SHALL handle asynchronous operations gracefully.
+The system SHALL handle asynchronous operations gracefully during auth bootstrap and dashboard fetches.
#### Scenario: Data fetching
- GIVEN a page loading data
- THEN:
- - Loading spinners/skeletons are shown
- - Error boundaries catch errors
- - Retry options are available on failure
+ - Loading states are shown while requests are in flight
+ - Errors are shown with retry affordance
+ - Initial auth-check loading prevents protected-layout flicker
### Requirement: HTTP Client Configuration
-
-The system SHALL configure HTTP requests properly.
+The system SHALL configure HTTP requests for cookie-based auth and unauthorized-session recovery.
#### Scenario: API communication
- GIVEN the frontend application
@@ -101,7 +96,7 @@ The system SHALL configure HTTP requests properly.
- Send credentials (cookies) with requests
- Handle 401 responses by redirecting to login
- Set appropriate content-type headers
- - Support request/response interceptors
+ - Support request/response interception in a shared client module
### Requirement: Dashboard Page