diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53e1488..9163439 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ permissions: env: REGISTRY: ghcr.io IMAGE_NAME: cartsnitch/cartsnitch + AUTH_IMAGE_NAME: cartsnitch/auth jobs: lint: @@ -107,6 +108,57 @@ jobs: git tag "v${{ steps.calver.outputs.version }}" git push origin "v${{ steps.calver.outputs.version }}" + build-and-push-auth: + runs-on: runners-cartsnitch + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate CalVer tag + id: calver + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + DATE_TAG=$(date -u +%Y.%m.%d) + EXISTING=$(git tag -l "v${DATE_TAG}*" | sort -V | tail -1) + if [ -z "$EXISTING" ]; then + VERSION="$DATE_TAG" + elif [ "$EXISTING" = "v${DATE_TAG}" ]; then + VERSION="${DATE_TAG}.2" + else + BUILD_NUM=$(echo "$EXISTING" | sed "s/v${DATE_TAG}\.//") + VERSION="${DATE_TAG}.$((BUILD_NUM + 1))" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (auth) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.AUTH_IMAGE_NAME }} + tags: | + type=sha,prefix=sha- + type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + + - name: Build and push auth Docker image + uses: docker/build-push-action@v6 + with: + context: ./auth + file: ./auth/Dockerfile + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + deploy-dev: runs-on: runners-cartsnitch needs: [build-and-push] diff --git a/.gitignore b/.gitignore index a547bf3..438657a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env # Editor directories and files .vscode/* diff --git a/CLAUDE.md b/CLAUDE.md index 623f979..8f6fc1a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ CartSnitch is a self-hosted grocery price intelligence platform. This repo (`car | Directory | Service | Purpose | |-----------|---------|---------| | `/` (root) | Frontend | React PWA, mobile-first (this directory) | +| `auth/` | Auth | Better-Auth Node.js service (session management, email/password, OAuth) | | `api/` | API Gateway | Frontend-facing REST API | | `common/` | Common | Shared Python models, schemas, Alembic migrations | | `receiptwitness/` | ReceiptWitness | Purchase data ingestion via retailer scrapers | @@ -166,9 +167,13 @@ frontend/ All data comes from the CartSnitch API gateway (`cartsnitch/api`). Base URL configured via environment variable `VITE_API_URL`. -- JWT auth: store access token in memory (not localStorage), refresh token in httpOnly cookie if possible, or secure storage. +- **Authentication via Better-Auth** (`auth/` service). Sessions are managed via httpOnly cookies — no tokens in localStorage or memory. + - Auth service URL configured via `VITE_AUTH_URL` (default: `http://localhost:3001`) + - Frontend uses `better-auth/react` client for sign-in, sign-up, sign-out, and `useSession()` hook + - API gateway validates sessions by querying the shared `sessions` table in Postgres + - Both cookie-based and Bearer token auth are supported (cookies for web, Bearer for API clients) - TanStack Query handles caching, background refetching, and optimistic updates. -- API client should handle 401 responses by attempting token refresh before retrying. +- API client sends `credentials: 'include'` on all requests to forward session cookies. ## Development Workflow diff --git a/api/alembic/versions/002_better_auth_tables.py b/api/alembic/versions/002_better_auth_tables.py new file mode 100644 index 0000000..aa5dd93 --- /dev/null +++ b/api/alembic/versions/002_better_auth_tables.py @@ -0,0 +1,101 @@ +"""Add Better-Auth tables and extend users table. + +Creates sessions, accounts, and verifications tables for Better-Auth. +Adds email_verified and image columns to existing users table. +Migrates password hashes from users.hashed_password to accounts.password. + +Revision ID: 002_better_auth_tables +Revises: 001_encrypt_session_data +Create Date: 2026-03-28 +""" + +import sqlalchemy as sa +from sqlalchemy import text + +from alembic import op + +revision = "002_better_auth_tables" +down_revision = "001_encrypt_session_data" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # --- Extend users table for Better-Auth compatibility --- + op.add_column("users", sa.Column("email_verified", sa.Boolean(), nullable=False, server_default="false")) + op.add_column("users", sa.Column("image", sa.Text(), nullable=True)) + + # --- Create sessions table --- + op.create_table( + "sessions", + sa.Column("id", sa.Text(), nullable=False), + sa.Column("token", sa.Text(), nullable=False), + sa.Column("user_id", sa.Text(), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("ip_address", sa.Text(), nullable=True), + sa.Column("user_agent", sa.Text(), nullable=True), + 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"), + ) + op.create_index("ix_sessions_token", "sessions", ["token"], unique=True) + op.create_index("ix_sessions_user_id", "sessions", ["user_id"]) + + # --- Create accounts table --- + op.create_table( + "accounts", + sa.Column("id", sa.Text(), nullable=False), + sa.Column("user_id", sa.Text(), nullable=False), + sa.Column("account_id", sa.Text(), nullable=False), + sa.Column("provider_id", sa.Text(), nullable=False), + sa.Column("access_token", sa.Text(), nullable=True), + sa.Column("refresh_token", sa.Text(), nullable=True), + sa.Column("access_token_expires_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("refresh_token_expires_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("scope", sa.Text(), nullable=True), + sa.Column("id_token", sa.Text(), nullable=True), + sa.Column("password", sa.Text(), nullable=True), + 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"), + ) + op.create_index("ix_accounts_user_id", "accounts", ["user_id"]) + + # --- Create verifications table --- + op.create_table( + "verifications", + sa.Column("id", sa.Text(), nullable=False), + sa.Column("identifier", sa.Text(), nullable=False), + sa.Column("value", sa.Text(), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=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"), + ) + + # --- Migrate existing password hashes to accounts table --- + # For each user with a hashed_password, create a 'credential' account row + conn = op.get_bind() + users = conn.execute( + text("SELECT id, hashed_password FROM users WHERE hashed_password IS NOT NULL") + ).fetchall() + + for user_id, hashed_password in users: + user_id_str = str(user_id) + conn.execute( + text( + "INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at) " + "VALUES (gen_random_uuid()::text, :user_id, :account_id, 'credential', :password, now(), now())" + ), + {"user_id": user_id_str, "account_id": user_id_str, "password": hashed_password}, + ) + + +def downgrade() -> None: + op.drop_table("verifications") + op.drop_table("accounts") + op.drop_index("ix_sessions_user_id", table_name="sessions") + op.drop_index("ix_sessions_token", table_name="sessions") + op.drop_table("sessions") + op.drop_column("users", "image") + op.drop_column("users", "email_verified") diff --git a/api/src/cartsnitch_api/auth/dependencies.py b/api/src/cartsnitch_api/auth/dependencies.py index 61735ee..93f8eb8 100644 --- a/api/src/cartsnitch_api/auth/dependencies.py +++ b/api/src/cartsnitch_api/auth/dependencies.py @@ -1,34 +1,88 @@ -"""FastAPI dependency injection for authentication.""" +"""FastAPI dependency injection for authentication. +Validates Better-Auth session tokens from cookies or Bearer header. +Sessions are verified by querying the shared sessions table directly. +""" + +from datetime import UTC, datetime from uuid import UUID -from fastapi import Depends, Header, HTTPException, status +from fastapi import Cookie, Depends, Header, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession -from cartsnitch_api.auth.jwt import decode_token from cartsnitch_api.config import settings +from cartsnitch_api.database import get_db -bearer_scheme = HTTPBearer() +# Keep Bearer scheme as optional — Better-Auth primarily uses cookies, +# but we support Bearer tokens for service-to-service or mobile clients. +bearer_scheme = HTTPBearer(auto_error=False) + +# Better-Auth session cookie name +SESSION_COOKIE_NAME = "better-auth.session_token" + + +async def _validate_session_token(token: str, db: AsyncSession) -> UUID: + """Validate a Better-Auth session token against the sessions table. + + Returns the user_id (as UUID) if the session is valid and not expired. + """ + result = await db.execute( + text("SELECT user_id, expires_at FROM sessions WHERE token = :token"), + {"token": token}, + ) + row = result.first() + + if not row: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid session token", + ) + + user_id, expires_at = row + if expires_at.tzinfo is None: + # Treat naive datetimes as UTC + expires_at = expires_at.replace(tzinfo=UTC) + + if expires_at < datetime.now(UTC): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Session expired", + ) + + return UUID(str(user_id)) async def get_current_user( - credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme), + request: Request, + credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), + db: AsyncSession = Depends(get_db), ) -> UUID: - try: - payload = decode_token(credentials.credentials) - except ValueError: + """Extract and validate the session token from cookie or Authorization header. + + Checks in order: + 1. Better-Auth session cookie (primary — web clients) + 2. Bearer token in Authorization header (fallback — API clients) + """ + token: str | None = None + + # 1. Check session cookie + cookie_token = request.cookies.get(SESSION_COOKIE_NAME) + if cookie_token: + token = cookie_token + + # 2. Fall back to Bearer header + if not token and credentials: + token = credentials.credentials + + if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid or expired token", - ) from None + detail="Authentication required", + ) - if payload.get("type") != "access": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token type", - ) from None - - return UUID(payload["sub"]) + return await _validate_session_token(token, db) async def verify_service_key(x_service_key: str = Header()) -> None: diff --git a/api/src/cartsnitch_api/auth/routes.py b/api/src/cartsnitch_api/auth/routes.py index ab34c3e..81cae2f 100644 --- a/api/src/cartsnitch_api/auth/routes.py +++ b/api/src/cartsnitch_api/auth/routes.py @@ -1,4 +1,9 @@ -"""Auth routes: register, login, refresh, me, update, delete.""" +"""Auth routes: user profile management. + +Registration, login, refresh, and session management are handled by +the Better-Auth service (auth/). This router provides user profile +endpoints that query our own user data from the shared database. +""" from uuid import UUID @@ -8,10 +13,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from cartsnitch_api.auth.dependencies import get_current_user from cartsnitch_api.database import get_db from cartsnitch_api.schemas import ( - LoginRequest, - RefreshRequest, - RegisterRequest, - TokenResponse, UpdateUserRequest, UserResponse, ) @@ -20,37 +21,6 @@ from cartsnitch_api.services.auth import AuthService router = APIRouter(prefix="/auth", tags=["auth"]) -@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) -async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)): - svc = AuthService(db) - try: - return await svc.register(body.email, body.password, body.display_name) - except ValueError as e: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) from e - - -@router.post("/login", response_model=TokenResponse) -async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)): - svc = AuthService(db) - try: - return await svc.login(body.email, body.password) - except ValueError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password" - ) from None - - -@router.post("/refresh", response_model=TokenResponse) -async def refresh(body: RefreshRequest, db: AsyncSession = Depends(get_db)): - svc = AuthService(db) - try: - return await svc.refresh(body.refresh_token) - except ValueError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" - ) from None - - @router.get("/me", response_model=UserResponse) async def get_me( user_id: UUID = Depends(get_current_user), diff --git a/api/src/cartsnitch_api/config.py b/api/src/cartsnitch_api/config.py index 52474b2..5111997 100644 --- a/api/src/cartsnitch_api/config.py +++ b/api/src/cartsnitch_api/config.py @@ -19,6 +19,8 @@ class Settings(BaseSettings): # Valid Fernet key for local dev — MUST be overridden in production fernet_key: str = "7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=" + auth_service_url: str = "http://auth:3001" + cors_origins: list[str] = ["http://localhost:3000", "https://cartsnitch.com"] receiptwitness_url: str = "http://receiptwitness:8001" diff --git a/api/src/cartsnitch_api/schemas.py b/api/src/cartsnitch_api/schemas.py index 19e351a..1ba727e 100644 --- a/api/src/cartsnitch_api/schemas.py +++ b/api/src/cartsnitch_api/schemas.py @@ -6,28 +6,8 @@ from uuid import UUID from pydantic import BaseModel, EmailStr, Field # ---------- Auth ---------- - - -class RegisterRequest(BaseModel): - email: EmailStr - password: str = Field(min_length=8, max_length=128) - display_name: str = Field(min_length=1, max_length=100) - - -class LoginRequest(BaseModel): - email: EmailStr - password: str - - -class RefreshRequest(BaseModel): - refresh_token: str - - -class TokenResponse(BaseModel): - access_token: str - refresh_token: str - token_type: str = "bearer" - expires_in: int +# Registration, login, and session management are handled by Better-Auth (auth/ service). +# These schemas are for the profile management endpoints only. class UpdateUserRequest(BaseModel): diff --git a/api/src/cartsnitch_api/services/auth.py b/api/src/cartsnitch_api/services/auth.py index 5ea6b77..91724af 100644 --- a/api/src/cartsnitch_api/services/auth.py +++ b/api/src/cartsnitch_api/services/auth.py @@ -1,67 +1,20 @@ -"""Auth service — user registration, login, token management.""" +"""Auth service — user profile management. + +Registration, login, token management, and session handling are now +handled by the Better-Auth service (auth/). This service provides +user lookup and profile update operations for the API gateway. +""" from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from cartsnitch_api.auth.jwt import create_access_token, create_refresh_token, decode_token -from cartsnitch_api.auth.passwords import hash_password, verify_password -from cartsnitch_api.config import settings - class AuthService: def __init__(self, db: AsyncSession) -> None: self.db = db - async def register(self, email: str, password: str, display_name: str) -> dict: - from cartsnitch_api.models import User - - existing = await self.db.execute(select(User).where(User.email == email)) - if existing.scalar_one_or_none(): - raise ValueError("Email already registered") - - user = User( - email=email, - hashed_password=hash_password(password), - display_name=display_name, - ) - self.db.add(user) - await self.db.commit() - await self.db.refresh(user) - - return self._make_token_response(user.id) - - async def login(self, email: str, password: str) -> dict: - from cartsnitch_api.models import User - - result = await self.db.execute(select(User).where(User.email == email)) - user = result.scalar_one_or_none() - if not user or not verify_password(password, user.hashed_password): - raise ValueError("Invalid email or password") - - return self._make_token_response(user.id) - - async def refresh(self, refresh_token: str) -> dict: - from cartsnitch_api.models import User - - try: - payload = decode_token(refresh_token) - except ValueError: - raise ValueError("Invalid refresh token") from None - - if payload.get("type") != "refresh": - raise ValueError("Invalid token type") from None - - user_id = UUID(payload["sub"]) - - # Verify the user still exists before issuing new tokens - result = await self.db.execute(select(User).where(User.id == user_id)) - if not result.scalar_one_or_none(): - raise ValueError("User no longer exists") - - return self._make_token_response(user_id) - async def get_user(self, user_id: UUID) -> dict: from cartsnitch_api.models import User @@ -115,11 +68,3 @@ class AuthService: await self.db.delete(user) await self.db.commit() - - def _make_token_response(self, user_id: UUID) -> dict: - return { - "access_token": create_access_token(user_id), - "refresh_token": create_refresh_token(user_id), - "token_type": "bearer", - "expires_in": settings.jwt_access_token_expire_minutes * 60, - } diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 9873903..61810e1 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,8 +1,16 @@ -"""Shared test fixtures with in-memory SQLite database.""" +"""Shared test fixtures with in-memory SQLite database. + +Session-based auth: tests create users and sessions directly in the DB, +matching the Better-Auth session validation flow. +""" + +import secrets +import uuid +from datetime import UTC, datetime, timedelta import pytest from httpx import ASGITransport, AsyncClient -from sqlalchemy import create_engine, event +from sqlalchemy import create_engine, event, text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import sessionmaker @@ -51,6 +59,46 @@ async def db_engine(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + # Create Better-Auth tables (not managed by SQLAlchemy models) + await conn.execute(text(""" + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + token TEXT NOT NULL UNIQUE, + user_id TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + ip_address TEXT, + user_agent TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + """)) + await conn.execute(text(""" + CREATE TABLE IF NOT EXISTS accounts ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + account_id TEXT NOT NULL, + provider_id TEXT NOT NULL, + access_token TEXT, + refresh_token TEXT, + access_token_expires_at TIMESTAMP, + refresh_token_expires_at TIMESTAMP, + scope TEXT, + id_token TEXT, + password TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + """)) + await conn.execute(text(""" + CREATE TABLE IF NOT EXISTS verifications ( + id TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + """)) yield engine @@ -85,17 +133,55 @@ async def client(db_engine): app.dependency_overrides.clear() +async def _create_test_user_and_session(client: AsyncClient, db_engine, **user_overrides) -> tuple[dict, str]: + """Create a test user and a valid session directly in the DB. + + Returns (user_dict, session_token). + """ + user_id = str(uuid.uuid4()) + email = user_overrides.get("email", "test@example.com") + display_name = user_overrides.get("display_name", "Test User") + session_token = secrets.token_urlsafe(32) + session_id = str(uuid.uuid4()) + now = datetime.now(UTC).isoformat() + expires = (datetime.now(UTC) + timedelta(days=7)).isoformat() + + async with db_engine.begin() as conn: + await conn.execute( + text( + "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) " + "VALUES (:id, :email, :hashed_password, :display_name, :email_verified, :created_at, :updated_at)" + ), + { + "id": user_id, + "email": email, + "hashed_password": "not-used-with-better-auth", + "display_name": display_name, + "email_verified": False, + "created_at": now, + "updated_at": now, + }, + ) + await conn.execute( + text( + "INSERT INTO sessions (id, token, user_id, expires_at, created_at, updated_at) " + "VALUES (:id, :token, :user_id, :expires_at, :created_at, :updated_at)" + ), + { + "id": session_id, + "token": session_token, + "user_id": user_id, + "expires_at": expires, + "created_at": now, + "updated_at": now, + }, + ) + + return {"id": user_id, "email": email, "display_name": display_name}, session_token + + @pytest.fixture -async def auth_headers(client): - """Register a test user and return auth headers.""" - resp = await client.post( - "/auth/register", - json={ - "email": "test@example.com", - "password": "testpass123", - "display_name": "Test User", - }, - ) - assert resp.status_code == 201 - token = resp.json()["access_token"] - return {"Authorization": f"Bearer {token}"} +async def auth_headers(client, db_engine): + """Create a test user with a valid session and return auth headers.""" + _, session_token = await _create_test_user_and_session(client, db_engine) + return {"Cookie": f"better-auth.session_token={session_token}"} diff --git a/api/tests/test_auth/test_auth_endpoints.py b/api/tests/test_auth/test_auth_endpoints.py index 878cbc5..7b096ae 100644 --- a/api/tests/test_auth/test_auth_endpoints.py +++ b/api/tests/test_auth/test_auth_endpoints.py @@ -1,146 +1,13 @@ -"""Integration tests for auth endpoints.""" +"""Integration tests for auth profile endpoints. + +Registration, login, and session management are handled by the Better-Auth +service. These tests cover the profile endpoints (GET/PATCH/DELETE /auth/me) +which validate sessions via the shared sessions table. +""" import pytest -@pytest.mark.asyncio -async def test_register_success(client): - resp = await client.post( - "/auth/register", - json={ - "email": "new@example.com", - "password": "securepass123", - "display_name": "New User", - }, - ) - assert resp.status_code == 201 - data = resp.json() - assert "access_token" in data - assert "refresh_token" in data - assert data["token_type"] == "bearer" - assert data["expires_in"] == 900 # 15 min * 60 - - -@pytest.mark.asyncio -async def test_register_duplicate_email(client): - await client.post( - "/auth/register", - json={ - "email": "dupe@example.com", - "password": "securepass123", - "display_name": "User One", - }, - ) - resp = await client.post( - "/auth/register", - json={ - "email": "dupe@example.com", - "password": "securepass456", - "display_name": "User Two", - }, - ) - assert resp.status_code == 409 - - -@pytest.mark.asyncio -async def test_register_short_password(client): - resp = await client.post( - "/auth/register", - json={ - "email": "short@example.com", - "password": "short", - "display_name": "Short Pass", - }, - ) - assert resp.status_code == 422 - - -@pytest.mark.asyncio -async def test_login_success(client): - await client.post( - "/auth/register", - json={ - "email": "login@example.com", - "password": "securepass123", - "display_name": "Login User", - }, - ) - resp = await client.post( - "/auth/login", - json={ - "email": "login@example.com", - "password": "securepass123", - }, - ) - assert resp.status_code == 200 - assert "access_token" in resp.json() - - -@pytest.mark.asyncio -async def test_login_wrong_password(client): - await client.post( - "/auth/register", - json={ - "email": "wrong@example.com", - "password": "securepass123", - "display_name": "Wrong Pass", - }, - ) - resp = await client.post( - "/auth/login", - json={ - "email": "wrong@example.com", - "password": "badpassword1", - }, - ) - assert resp.status_code == 401 - - -@pytest.mark.asyncio -async def test_login_nonexistent_user(client): - resp = await client.post( - "/auth/login", - json={ - "email": "ghost@example.com", - "password": "doesntmatter", - }, - ) - assert resp.status_code == 401 - - -@pytest.mark.asyncio -async def test_refresh_token(client): - reg = await client.post( - "/auth/register", - json={ - "email": "refresh@example.com", - "password": "securepass123", - "display_name": "Refresh User", - }, - ) - refresh_token = reg.json()["refresh_token"] - - resp = await client.post( - "/auth/refresh", - json={ - "refresh_token": refresh_token, - }, - ) - assert resp.status_code == 200 - assert "access_token" in resp.json() - - -@pytest.mark.asyncio -async def test_refresh_with_invalid_token(client): - resp = await client.post( - "/auth/refresh", - json={ - "refresh_token": "invalid.token.here", - }, - ) - assert resp.status_code == 401 - - @pytest.mark.asyncio async def test_get_me(client, auth_headers): resp = await client.get("/auth/me", headers=auth_headers) @@ -155,7 +22,32 @@ async def test_get_me(client, auth_headers): @pytest.mark.asyncio async def test_get_me_unauthorized(client): resp = await client.get("/auth/me") - assert resp.status_code in (401, 403) # No auth header + assert resp.status_code in (401, 403) + + +@pytest.mark.asyncio +async def test_get_me_invalid_session(client): + resp = await client.get( + "/auth/me", + headers={"Cookie": "better-auth.session_token=invalid-token"}, + ) + assert resp.status_code == 401 + + +@pytest.mark.asyncio +async def test_get_me_with_bearer_token(client, db_engine): + """Session tokens can also be passed as Bearer tokens for API clients.""" + from tests.conftest import _create_test_user_and_session + + _, session_token = await _create_test_user_and_session( + client, db_engine, email="bearer@example.com", display_name="Bearer User" + ) + resp = await client.get( + "/auth/me", + headers={"Authorization": f"Bearer {session_token}"}, + ) + assert resp.status_code == 200 + assert resp.json()["email"] == "bearer@example.com" @pytest.mark.asyncio @@ -163,9 +55,7 @@ async def test_update_me(client, auth_headers): resp = await client.patch( "/auth/me", headers=auth_headers, - json={ - "display_name": "Updated Name", - }, + json={"display_name": "Updated Name"}, ) assert resp.status_code == 200 assert resp.json()["display_name"] == "Updated Name" @@ -176,34 +66,58 @@ async def test_delete_me(client, auth_headers): resp = await client.delete("/auth/me", headers=auth_headers) assert resp.status_code == 204 - # Verify user is gone (token still valid but user deleted) + # Session is still valid but user is gone resp = await client.get("/auth/me", headers=auth_headers) assert resp.status_code == 404 @pytest.mark.asyncio -async def test_refresh_after_delete_fails(client): - """Refresh token for a deleted user must be rejected.""" - reg = await client.post( - "/auth/register", - json={ - "email": "ghost@example.com", - "password": "securepass123", - "display_name": "Ghost User", - }, - ) - tokens = reg.json() - headers = {"Authorization": f"Bearer {tokens['access_token']}"} +async def test_expired_session_rejected(client, db_engine): + """Expired sessions must be rejected.""" + import secrets + import uuid + from datetime import UTC, datetime, timedelta - # Delete the user - resp = await client.delete("/auth/me", headers=headers) - assert resp.status_code == 204 + from sqlalchemy import text - # Refresh token should now fail - resp = await client.post( - "/auth/refresh", - json={ - "refresh_token": tokens["refresh_token"], - }, + user_id = str(uuid.uuid4()) + session_token = secrets.token_urlsafe(32) + now = datetime.now(UTC).isoformat() + expired = (datetime.now(UTC) - timedelta(hours=1)).isoformat() + + async with db_engine.begin() as conn: + await conn.execute( + text( + "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) " + "VALUES (:id, :email, :hp, :dn, :ev, :ca, :ua)" + ), + { + "id": user_id, + "email": "expired@example.com", + "hp": "unused", + "dn": "Expired User", + "ev": False, + "ca": now, + "ua": now, + }, + ) + await conn.execute( + text( + "INSERT INTO sessions (id, token, user_id, expires_at, created_at, updated_at) " + "VALUES (:id, :token, :uid, :ea, :ca, :ua)" + ), + { + "id": str(uuid.uuid4()), + "token": session_token, + "uid": user_id, + "ea": expired, + "ca": now, + "ua": now, + }, + ) + + resp = await client.get( + "/auth/me", + headers={"Cookie": f"better-auth.session_token={session_token}"}, ) assert resp.status_code == 401 diff --git a/api/tests/test_e2e/conftest.py b/api/tests/test_e2e/conftest.py index f1390fd..d352344 100644 --- a/api/tests/test_e2e/conftest.py +++ b/api/tests/test_e2e/conftest.py @@ -10,9 +10,9 @@ from decimal import Decimal from uuid import UUID import pytest +from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker -from cartsnitch_api.auth.jwt import decode_token from cartsnitch_api.models import ( Coupon, NormalizedProduct, @@ -126,10 +126,16 @@ async def seed_data(db_engine, auth_headers): session.add_all(prices) await session.flush() - # -- Purchases (need the user_id from the registered test user) -- - token = auth_headers["Authorization"].split(" ")[1] - payload = decode_token(token) - user_id = UUID(payload["sub"]) + # -- Get the user_id from the session token in auth_headers -- + cookie_str = auth_headers.get("Cookie", "") + session_token = cookie_str.split("=", 1)[1] if "=" in cookie_str else "" + + result = await session.execute( + text("SELECT user_id FROM sessions WHERE token = :token"), + {"token": session_token}, + ) + row = result.first() + user_id = UUID(row[0]) purchase1 = Purchase( user_id=user_id, diff --git a/api/tests/test_e2e/test_auth_validation.py b/api/tests/test_e2e/test_auth_validation.py index bbded83..f0e38cd 100644 --- a/api/tests/test_e2e/test_auth_validation.py +++ b/api/tests/test_e2e/test_auth_validation.py @@ -1,132 +1,103 @@ -"""E2E: Auth and token validation flows.""" +"""E2E: Auth and session validation flows. -import asyncio +Registration and login are handled by the Better-Auth service. +These tests validate session token handling at the API gateway level. +""" import pytest - -@pytest.mark.asyncio -class TestAuthRegistrationLogin: - """Full registration → login → token refresh → profile flow.""" - - async def test_full_auth_lifecycle(self, client, db_engine): - """Register → login → get profile → refresh → get profile again.""" - # Register - reg = await client.post( - "/auth/register", - json={ - "email": "lifecycle@example.com", - "password": "securepass123", - "display_name": "Lifecycle User", - }, - ) - assert reg.status_code == 201 - tokens = reg.json() - assert "access_token" in tokens - assert "refresh_token" in tokens - assert tokens["token_type"] == "bearer" - assert tokens["expires_in"] > 0 - - headers = {"Authorization": f"Bearer {tokens['access_token']}"} - - # Get profile with access token - me = await client.get("/auth/me", headers=headers) - assert me.status_code == 200 - assert me.json()["email"] == "lifecycle@example.com" - assert me.json()["display_name"] == "Lifecycle User" - - # Sleep 1s so the new token has a different exp than the registration token - await asyncio.sleep(1) - - # Login with same credentials - login = await client.post( - "/auth/login", - json={"email": "lifecycle@example.com", "password": "securepass123"}, - ) - assert login.status_code == 200 - login_tokens = login.json() - assert login_tokens["access_token"] != tokens["access_token"] - - # Refresh token - refresh = await client.post( - "/auth/refresh", - json={"refresh_token": tokens["refresh_token"]}, - ) - assert refresh.status_code == 200 - new_tokens = refresh.json() - assert new_tokens["access_token"] != tokens["access_token"] - - # Use refreshed token to access profile - new_headers = {"Authorization": f"Bearer {new_tokens['access_token']}"} - me2 = await client.get("/auth/me", headers=new_headers) - assert me2.status_code == 200 - assert me2.json()["email"] == "lifecycle@example.com" +from tests.conftest import _create_test_user_and_session @pytest.mark.asyncio -class TestTokenValidation: - """Token edge cases and error responses.""" +class TestSessionValidation: + """Session edge cases and error responses.""" - async def test_expired_token_rejected(self, client, db_engine): - """Manually craft an expired token and verify rejection.""" - import uuid - from datetime import UTC, datetime, timedelta - - from jose import jwt - - from cartsnitch_api.config import settings - - payload = { - "sub": str(uuid.uuid4()), - "exp": datetime.now(UTC) - timedelta(minutes=5), - "type": "access", - } - token = jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) - resp = await client.get("/auth/me", headers={"Authorization": f"Bearer {token}"}) + async def test_invalid_session_token_rejected(self, client, db_engine): + resp = await client.get( + "/auth/me", + headers={"Cookie": "better-auth.session_token=not-a-real-token"}, + ) assert resp.status_code == 401 - async def test_invalid_token_rejected(self, client, db_engine): - resp = await client.get("/auth/me", headers={"Authorization": "Bearer not-a-real-token"}) - assert resp.status_code == 401 - - async def test_missing_auth_header(self, client, db_engine): + async def test_missing_auth(self, client, db_engine): resp = await client.get("/auth/me") assert resp.status_code in (401, 403) - async def test_refresh_token_cannot_access_endpoints(self, client, db_engine): - """A refresh token should not work as an access token.""" - reg = await client.post( - "/auth/register", - json={ - "email": "refresh-test@example.com", - "password": "securepass123", - "display_name": "Refresh Test", - }, + async def test_bearer_token_also_works(self, client, db_engine): + """Session tokens passed as Bearer tokens should also be accepted.""" + _, session_token = await _create_test_user_and_session( + client, db_engine, email="bearer@e2e.com", display_name="Bearer E2E" ) - refresh_token = reg.json()["refresh_token"] - resp = await client.get("/auth/me", headers={"Authorization": f"Bearer {refresh_token}"}) - assert resp.status_code == 401 - - async def test_deleted_user_token_invalid(self, client, db_engine): - """After deleting an account, tokens should no longer work.""" - reg = await client.post( - "/auth/register", - json={ - "email": "delete-me@example.com", - "password": "securepass123", - "display_name": "Delete Me", - }, + resp = await client.get( + "/auth/me", + headers={"Authorization": f"Bearer {session_token}"}, ) - tokens = reg.json() - headers = {"Authorization": f"Bearer {tokens['access_token']}"} + assert resp.status_code == 200 + assert resp.json()["email"] == "bearer@e2e.com" + + async def test_deleted_user_session_returns_not_found(self, client, db_engine): + """After deleting a user, their session should result in 404 for profile.""" + _, session_token = await _create_test_user_and_session( + client, db_engine, email="delete-me@e2e.com", display_name="Delete Me" + ) + headers = {"Cookie": f"better-auth.session_token={session_token}"} - # Delete account delete_resp = await client.delete("/auth/me", headers=headers) assert delete_resp.status_code == 204 - # Profile should fail me = await client.get("/auth/me", headers=headers) - assert me.status_code in (401, 404) + assert me.status_code == 404 + + async def test_expired_session_rejected(self, client, db_engine): + """Expired sessions must be rejected.""" + import secrets + import uuid + from datetime import UTC, datetime, timedelta + + from sqlalchemy import text + + user_id = str(uuid.uuid4()) + session_token = secrets.token_urlsafe(32) + now = datetime.now(UTC).isoformat() + expired = (datetime.now(UTC) - timedelta(hours=1)).isoformat() + + async with db_engine.begin() as conn: + await conn.execute( + text( + "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) " + "VALUES (:id, :email, :hp, :dn, :ev, :ca, :ua)" + ), + { + "id": user_id, + "email": "expired@e2e.com", + "hp": "unused", + "dn": "Expired User", + "ev": False, + "ca": now, + "ua": now, + }, + ) + await conn.execute( + text( + "INSERT INTO sessions (id, token, user_id, expires_at, created_at, updated_at) " + "VALUES (:id, :token, :uid, :ea, :ca, :ua)" + ), + { + "id": str(uuid.uuid4()), + "token": session_token, + "uid": user_id, + "ea": expired, + "ca": now, + "ua": now, + }, + ) + + resp = await client.get( + "/auth/me", + headers={"Cookie": f"better-auth.session_token={session_token}"}, + ) + assert resp.status_code == 401 @pytest.mark.asyncio @@ -154,60 +125,38 @@ class TestAuthProtectedEndpoints: class TestCrossUserDataIsolation: """Verify that users cannot access other users' data.""" - async def test_user_b_cannot_access_user_a_purchases(self, client, seed_data): - """Register a second user and verify they cannot see User A's purchases.""" - # User A's purchase (from seed_data) + async def test_user_b_cannot_access_user_a_purchases(self, client, db_engine, seed_data): + """A second user cannot see User A's purchases.""" purchase_id = str(seed_data["purchases"]["meijer_trip"].id) - # Register User B - reg = await client.post( - "/auth/register", - json={ - "email": "userb@example.com", - "password": "securepass123", - "display_name": "User B", - }, + _, session_token = await _create_test_user_and_session( + client, db_engine, email="userb@e2e.com", display_name="User B" ) - assert reg.status_code == 201 - user_b_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"} + user_b_headers = {"Cookie": f"better-auth.session_token={session_token}"} - # User B tries to access User A's specific purchase resp = await client.get(f"/purchases/{purchase_id}", headers=user_b_headers) assert resp.status_code in (403, 404), ( "User B should not be able to access User A's purchase" ) - async def test_user_b_purchase_list_is_empty(self, client, seed_data): - """A new user should see no purchases (not User A's purchases).""" - reg = await client.post( - "/auth/register", - json={ - "email": "userc@example.com", - "password": "securepass123", - "display_name": "User C", - }, + async def test_user_b_purchase_list_is_empty(self, client, db_engine, seed_data): + """A new user should see no purchases.""" + _, session_token = await _create_test_user_and_session( + client, db_engine, email="userc@e2e.com", display_name="User C" ) - assert reg.status_code == 201 - user_c_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"} + user_c_headers = {"Cookie": f"better-auth.session_token={session_token}"} resp = await client.get("/purchases", headers=user_c_headers) assert resp.status_code == 200 assert len(resp.json()) == 0, "New user should have no purchases" - async def test_user_b_stores_isolated(self, client, seed_data): + async def test_user_b_stores_isolated(self, client, db_engine, seed_data): """User B's connected stores should be independent from User A.""" - reg = await client.post( - "/auth/register", - json={ - "email": "userd@example.com", - "password": "securepass123", - "display_name": "User D", - }, + _, session_token = await _create_test_user_and_session( + client, db_engine, email="userd@e2e.com", display_name="User D" ) - assert reg.status_code == 201 - user_d_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"} + user_d_headers = {"Cookie": f"better-auth.session_token={session_token}"} - # User D should have no connected stores resp = await client.get("/me/stores", headers=user_d_headers) assert resp.status_code == 200 assert len(resp.json()) == 0, "New user should have no connected stores" diff --git a/api/tests/test_routes/test_purchases.py b/api/tests/test_routes/test_purchases.py index 14d5eb6..2b1f47b 100644 --- a/api/tests/test_routes/test_purchases.py +++ b/api/tests/test_routes/test_purchases.py @@ -1,26 +1,25 @@ """Integration tests for purchase endpoints.""" +import secrets import uuid -from datetime import date +from datetime import UTC, date, datetime, timedelta from decimal import Decimal import pytest +from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker -from cartsnitch_api.auth.jwt import create_access_token from cartsnitch_api.models import Purchase, PurchaseItem, Store, User @pytest.fixture async def purchase_data(db_engine): - """Seed a user, store, purchase, and items.""" + """Seed a user, store, purchase, items, and a valid session.""" factory = async_sessionmaker(db_engine, class_=AsyncSession, expire_on_commit=False) async with factory() as session: - from cartsnitch_api.auth.passwords import hash_password - user = User( email="buyer@example.com", - hashed_password=hash_password("testpass123"), + hashed_password="not-used-with-better-auth", display_name="Buyer", ) store = Store(name="Kroger", slug="kroger") @@ -50,13 +49,33 @@ async def purchase_data(db_engine): session.add(item) await session.commit() - token = create_access_token(user.id) - return { - "user": user, - "store": store, - "purchase": purchase, - "headers": {"Authorization": f"Bearer {token}"}, - } + # Create a session token directly in the sessions table + session_token = secrets.token_urlsafe(32) + now = datetime.now(UTC).isoformat() + expires = (datetime.now(UTC) + timedelta(days=7)).isoformat() + + async with db_engine.begin() as conn: + await conn.execute( + text( + "INSERT INTO sessions (id, token, user_id, expires_at, created_at, updated_at) " + "VALUES (:id, :token, :user_id, :expires_at, :created_at, :updated_at)" + ), + { + "id": str(uuid.uuid4()), + "token": session_token, + "user_id": str(user.id), + "expires_at": expires, + "created_at": now, + "updated_at": now, + }, + ) + + return { + "user": user, + "store": store, + "purchase": purchase, + "headers": {"Cookie": f"better-auth.session_token={session_token}"}, + } @pytest.mark.asyncio diff --git a/auth/.env.example b/auth/.env.example new file mode 100644 index 0000000..6e16447 --- /dev/null +++ b/auth/.env.example @@ -0,0 +1,11 @@ +# Required: Generate with `openssl rand -base64 32` +BETTER_AUTH_SECRET=change-me-in-production-min-32-chars!! + +# Base URL of the auth service +BETTER_AUTH_URL=http://localhost:3001 + +# Shared PostgreSQL database +DATABASE_URL=postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch + +# Port the auth service listens on +PORT=3001 diff --git a/auth/Dockerfile b/auth/Dockerfile new file mode 100644 index 0000000..1028e89 --- /dev/null +++ b/auth/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-alpine AS builder +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src/ src/ +RUN npm run build + +FROM node:22-alpine +WORKDIR /app +ENV NODE_ENV=production +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev +COPY --from=builder /app/dist/ dist/ +USER 101 +EXPOSE 3001 +CMD ["node", "dist/index.js"] diff --git a/auth/package-lock.json b/auth/package-lock.json new file mode 100644 index 0000000..1f7bfc9 --- /dev/null +++ b/auth/package-lock.json @@ -0,0 +1,1754 @@ +{ + "name": "@cartsnitch/auth", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@cartsnitch/auth", + "version": "0.1.0", + "dependencies": { + "bcrypt": "^5.1.1", + "better-auth": "^1.2.0", + "pg": "^8.13.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/node": "^22.0.0", + "@types/pg": "^8.11.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@better-auth/core": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.5.6.tgz", + "integrity": "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.39.0", + "@standard-schema/spec": "^1.1.0", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@better-auth/utils": "0.3.1", + "@better-fetch/fetch": "1.1.21", + "@cloudflare/workers-types": ">=4", + "@opentelemetry/api": "^1.9.0", + "better-call": "1.3.2", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@better-auth/drizzle-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.5.6.tgz", + "integrity": "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "drizzle-orm": ">=0.41.0" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/@better-auth/kysely-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.5.6.tgz", + "integrity": "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "kysely": "^0.27.0 || ^0.28.0" + }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.5.6.tgz", + "integrity": "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.5.6.tgz", + "integrity": "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.5.6.tgz", + "integrity": "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.5.6.tgz", + "integrity": "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "0.3.1", + "@better-fetch/fetch": "1.1.21" + }, + "peerDependencies": { + "@better-auth/core": "1.5.6" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz", + "integrity": "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==", + "license": "MIT" + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/better-auth": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.5.6.tgz", + "integrity": "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/drizzle-adapter": "1.5.6", + "@better-auth/kysely-adapter": "1.5.6", + "@better-auth/memory-adapter": "1.5.6", + "@better-auth/mongo-adapter": "1.5.6", + "@better-auth/prisma-adapter": "1.5.6", + "@better-auth/telemetry": "1.5.6", + "@better-auth/utils": "0.3.1", + "@better-fetch/fetch": "1.1.21", + "@noble/ciphers": "^2.1.1", + "@noble/hashes": "^2.0.1", + "better-call": "1.3.2", + "defu": "^6.1.4", + "jose": "^6.1.3", + "kysely": "^0.28.12", + "nanostores": "^1.1.1", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@lynx-js/react": "*", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "@sveltejs/kit": "^2.0.0", + "@tanstack/react-start": "^1.0.0", + "@tanstack/solid-start": "^1.0.0", + "better-sqlite3": "^12.0.0", + "drizzle-kit": ">=0.31.4", + "drizzle-orm": ">=0.41.0", + "mongodb": "^6.0.0 || ^7.0.0", + "mysql2": "^3.0.0", + "next": "^14.0.0 || ^15.0.0 || ^16.0.0", + "pg": "^8.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "solid-js": "^1.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "@tanstack/react-start": { + "optional": true + }, + "@tanstack/solid-start": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-kit": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "next": { + "optional": true + }, + "pg": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vitest": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-call": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.2.tgz", + "integrity": "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "^0.3.1", + "@better-fetch/fetch": "^1.1.21", + "rou3": "^0.7.12", + "set-cookie-parser": "^3.0.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "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/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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", + "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/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "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/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.", + "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==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/kysely": { + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz", + "integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/nanostores": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz", + "integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/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", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rou3": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", + "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar 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", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/auth/package.json b/auth/package.json new file mode 100644 index 0000000..0071e27 --- /dev/null +++ b/auth/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cartsnitch/auth", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "generate": "npx @better-auth/cli generate" + }, + "dependencies": { + "better-auth": "^1.2.0", + "pg": "^8.13.0", + "bcrypt": "^5.1.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/pg": "^8.11.0", + "@types/bcrypt": "^5.0.2", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/auth/src/auth.ts b/auth/src/auth.ts new file mode 100644 index 0000000..1215cdb --- /dev/null +++ b/auth/src/auth.ts @@ -0,0 +1,90 @@ +import { betterAuth } from "better-auth"; +import bcrypt from "bcrypt"; +import pg from "pg"; + +const { Pool } = pg; + +const pool = new Pool({ + connectionString: + process.env.DATABASE_URL ?? + "postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch", +}); + +const secret = process.env.BETTER_AUTH_SECRET; +if (!secret) { + throw new Error("BETTER_AUTH_SECRET environment variable is required"); +} + +export const auth = betterAuth({ + database: pool, + basePath: "/auth", + secret, + baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3001", + + emailAndPassword: { + enabled: true, + minPasswordLength: 8, + maxPasswordLength: 128, + password: { + hash: async (password: string) => { + return bcrypt.hash(password, 10); + }, + verify: async (data: { hash: string; password: string }) => { + return bcrypt.compare(data.password, data.hash); + }, + }, + }, + + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // refresh after 1 day + cookieCache: { + enabled: true, + maxAge: 5 * 60, // 5-minute cookie cache + }, + }, + + user: { + modelName: "users", + fields: { + name: "display_name", + emailVerified: "email_verified", + image: "image", + createdAt: "created_at", + updatedAt: "updated_at", + }, + }, + + account: { + modelName: "accounts", + fields: { + userId: "user_id", + accountId: "account_id", + providerId: "provider_id", + accessToken: "access_token", + refreshToken: "refresh_token", + accessTokenExpiresAt: "access_token_expires_at", + refreshTokenExpiresAt: "refresh_token_expires_at", + idToken: "id_token", + createdAt: "created_at", + updatedAt: "updated_at", + }, + }, + + verification: { + modelName: "verifications", + fields: { + expiresAt: "expires_at", + createdAt: "created_at", + updatedAt: "updated_at", + }, + }, + + trustedOrigins: [ + "http://localhost:3000", + "http://localhost:5173", + "https://cartsnitch.com", + "https://cartsnitch.farh.net", + "https://cartsnitch.dev.farh.net", + ], +}); diff --git a/auth/src/index.ts b/auth/src/index.ts new file mode 100644 index 0000000..843a97c --- /dev/null +++ b/auth/src/index.ts @@ -0,0 +1,23 @@ +import { createServer } from "node:http"; +import { toNodeHandler } from "better-auth/node"; +import { auth } from "./auth.js"; + +const port = parseInt(process.env.PORT ?? "3001", 10); + +const handler = toNodeHandler(auth); + +const server = createServer(async (req, res) => { + // Health check + if (req.url === "/health" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + return; + } + + // All /auth/* routes handled by Better-Auth + await handler(req, res); +}); + +server.listen(port, "0.0.0.0", () => { + console.log(`CartSnitch auth service listening on port ${port}`); +}); diff --git a/auth/tsconfig.json b/auth/tsconfig.json new file mode 100644 index 0000000..764b72a --- /dev/null +++ b/auth/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "resolveJsonModule": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000..75ff58a --- /dev/null +++ b/common/README.md @@ -0,0 +1,28 @@ +# CartSnitch Common + +Shared models, schemas, and utilities for CartSnitch services. + +## Test Users + +The following users are seeded by `cartsnitch-seed` and can be used for local development and UAT. + +| Email | Password | Display Name | Notes | +|---|---|---|---| +| `uat@cartsnitch.com` | `CartSnitch-UAT-2026!` | UAT Tester | Primary UAT account. Use for regression testing in the CartSnitch frontend. Created by the seed runner via Better-Auth's bcrypt path — credentials work against the live auth service. Idempotent; re-running the seed skips this user if it already exists. | + +### Running the Seed + +```bash +# Install with seed dependencies +pip install -e "cartsnitch-common[seed]" + +# Run (requires CARTSNITCH_DATABASE_URL_SYNC) +CARTSNITCH_DATABASE_URL_SYNC=postgresql://user:pass@localhost:5432/cartsnitch \ + cartsnitch-seed +``` + +### Architecture + +- **Models** live in `src/cartsnitch_common/models/` +- **Alembic migrations** run via the `api` service (`api/alembic/`) +- **Seed runner** runs via `cartsnitch-seed` (installed as a package entry point) diff --git a/common/pyproject.toml b/common/pyproject.toml index ee348c5..5aa0309 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -27,6 +27,7 @@ dev = [ ] seed = [ "faker>=33.0,<34.0", + "bcrypt>=4.0,<6.0", ] [project.scripts] diff --git a/common/src/cartsnitch_common/models/user.py b/common/src/cartsnitch_common/models/user.py index e2b1bfb..5e35e5a 100644 --- a/common/src/cartsnitch_common/models/user.py +++ b/common/src/cartsnitch_common/models/user.py @@ -4,7 +4,7 @@ import uuid from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import JSON, DateTime, ForeignKey, String, UniqueConstraint +from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from cartsnitch_common.constants import AccountStatus @@ -23,6 +23,8 @@ class User(UUIDPrimaryKeyMixin, TimestampMixin, Base): email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str | None] = mapped_column(String(100)) + email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") + image: Mapped[str | None] = mapped_column(Text, nullable=True) # Relationships store_accounts: Mapped[list["UserStoreAccount"]] = relationship(back_populates="user") diff --git a/common/src/cartsnitch_common/seed/runner.py b/common/src/cartsnitch_common/seed/runner.py index c2b7784..d804b28 100644 --- a/common/src/cartsnitch_common/seed/runner.py +++ b/common/src/cartsnitch_common/seed/runner.py @@ -2,8 +2,10 @@ import random import time +import uuid from typing import Any +import bcrypt from faker import Faker from sqlalchemy import text from sqlalchemy.orm import Session @@ -184,6 +186,65 @@ def run_seed( session.commit() + _seed_uat_user(session) + elapsed = time.monotonic() - t0 _log("") _log(f"Seed complete in {elapsed:.1f}s") + + +# --------------------------------------------------------------------------- +# UAT seed user +# --------------------------------------------------------------------------- + +UAT_EMAIL = "uat@cartsnitch.com" +UAT_PASSWORD = "CartSnitch-UAT-2026!" +UAT_DISPLAY_NAME = "UAT Tester" +UAT_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") + + +def _seed_uat_user(session: Session) -> None: + """Insert or verify the dedicated UAT test user. + + The user is created via Better-Auth's bcrypt hashing path so credentials + work against the live auth service. Idempotent — skips if the user already + exists. + """ + existing = session.execute( + text("SELECT id FROM users WHERE email = :email"), + {"email": UAT_EMAIL}, + ).fetchone() + + if existing is not None: + _log(f"UAT user {UAT_EMAIL} already exists — skipping") + return + + password_hash = bcrypt.hashpw(UAT_PASSWORD.encode(), bcrypt.gensalt()).decode() + + session.execute( + text( + "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) " + "VALUES (:id, :email, :hashed_password, :display_name, true, now(), now())" + ), + { + "id": str(UAT_USER_ID), + "email": UAT_EMAIL, + "hashed_password": password_hash, + "display_name": UAT_DISPLAY_NAME, + }, + ) + + session.execute( + text( + "INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at) " + "VALUES (gen_random_uuid()::text, :user_id, :account_id, 'credential', :password, now(), now())" + ), + { + "user_id": str(UAT_USER_ID), + "account_id": str(UAT_USER_ID), + "password": password_hash, + }, + ) + + session.commit() + _log(f"UAT user {UAT_EMAIL} created") diff --git a/package-lock.json b/package-lock.json index 41b0cad..8200b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@tanstack/react-query": "^5.0.0", + "better-auth": "^1.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.0", @@ -48,7 +49,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.3", @@ -62,7 +63,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/@babel/code-frame": { @@ -1621,11 +1622,123 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/core": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.5.6.tgz", + "integrity": "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.39.0", + "@standard-schema/spec": "^1.1.0", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@better-auth/utils": "0.3.1", + "@better-fetch/fetch": "1.1.21", + "@cloudflare/workers-types": ">=4", + "@opentelemetry/api": "^1.9.0", + "better-call": "1.3.2", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@better-auth/kysely-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.5.6.tgz", + "integrity": "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "kysely": "^0.27.0 || ^0.28.0" + }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.5.6.tgz", + "integrity": "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.5.6.tgz", + "integrity": "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.5.6.tgz", + "integrity": "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.5.6.tgz", + "integrity": "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "0.3.1", + "@better-fetch/fetch": "1.1.21" + }, + "peerDependencies": { + "@better-auth/core": "1.5.6" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz", + "integrity": "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==", + "license": "MIT" + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1645,7 +1758,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1669,7 +1782,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1697,7 +1810,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1720,7 +1833,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1743,7 +1856,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1760,7 +1872,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1777,7 +1888,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1794,7 +1904,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1811,7 +1920,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1828,7 +1936,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1845,7 +1952,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1862,7 +1968,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1879,7 +1984,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1896,7 +2000,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1913,7 +2016,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1930,7 +2032,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1947,7 +2048,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1964,7 +2064,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1981,7 +2080,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1998,7 +2096,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2015,7 +2112,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2032,7 +2128,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2049,7 +2144,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2066,7 +2160,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2083,7 +2176,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2100,7 +2192,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2117,7 +2208,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2134,7 +2224,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2151,7 +2240,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2168,7 +2256,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2401,7 +2488,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2423,7 +2510,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2433,7 +2520,7 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2444,20 +2531,63 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -2586,7 +2716,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2600,7 +2729,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2614,7 +2742,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2628,7 +2755,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2642,7 +2768,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2656,7 +2781,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2670,7 +2794,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2684,7 +2807,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2698,7 +2820,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2712,7 +2833,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2726,7 +2846,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2740,7 +2859,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2754,7 +2872,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2768,7 +2885,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2782,7 +2898,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2796,7 +2911,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2810,7 +2924,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2824,7 +2937,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2838,7 +2950,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2852,7 +2963,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2866,7 +2976,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2880,7 +2989,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2894,7 +3002,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2908,7 +3015,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2922,7 +3028,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3395,7 +3500,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -3469,14 +3574,14 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, + "devOptional": 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, + "devOptional": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -3490,7 +3595,7 @@ "version": "24.12.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -3864,7 +3969,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", @@ -3881,7 +3986,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/spy": "3.2.4", @@ -3908,7 +4013,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tinyrainbow": "^2.0.0" @@ -3921,7 +4026,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/utils": "3.2.4", @@ -3936,7 +4041,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "3.2.4", @@ -3951,7 +4056,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tinyspy": "^4.0.3" @@ -3964,7 +4069,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "3.2.4", @@ -3979,7 +4084,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4002,7 +4107,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14" @@ -4112,7 +4217,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -4139,7 +4244,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -4230,6 +4335,153 @@ "node": ">=6.0.0" } }, + "node_modules/better-auth": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.5.6.tgz", + "integrity": "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/drizzle-adapter": "1.5.6", + "@better-auth/kysely-adapter": "1.5.6", + "@better-auth/memory-adapter": "1.5.6", + "@better-auth/mongo-adapter": "1.5.6", + "@better-auth/prisma-adapter": "1.5.6", + "@better-auth/telemetry": "1.5.6", + "@better-auth/utils": "0.3.1", + "@better-fetch/fetch": "1.1.21", + "@noble/ciphers": "^2.1.1", + "@noble/hashes": "^2.0.1", + "better-call": "1.3.2", + "defu": "^6.1.4", + "jose": "^6.1.3", + "kysely": "^0.28.12", + "nanostores": "^1.1.1", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@lynx-js/react": "*", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "@sveltejs/kit": "^2.0.0", + "@tanstack/react-start": "^1.0.0", + "@tanstack/solid-start": "^1.0.0", + "better-sqlite3": "^12.0.0", + "drizzle-kit": ">=0.31.4", + "drizzle-orm": ">=0.41.0", + "mongodb": "^6.0.0 || ^7.0.0", + "mysql2": "^3.0.0", + "next": "^14.0.0 || ^15.0.0 || ^16.0.0", + "pg": "^8.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "solid-js": "^1.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "@tanstack/react-start": { + "optional": true + }, + "@tanstack/solid-start": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-kit": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "next": { + "optional": true + }, + "pg": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vitest": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-auth/node_modules/@better-auth/drizzle-adapter": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.5.6.tgz", + "integrity": "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "1.5.6", + "@better-auth/utils": "^0.3.0", + "drizzle-orm": ">=0.41.0" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/better-call": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.2.tgz", + "integrity": "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "^0.3.1", + "@better-fetch/fetch": "^1.1.21", + "rou3": "^0.7.12", + "set-cookie-parser": "^3.0.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/better-call/node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4279,14 +4531,14 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -4315,7 +4567,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4377,7 +4629,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", @@ -4411,7 +4663,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 16" @@ -4450,7 +4702,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4463,7 +4715,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/common-tags": { @@ -4553,7 +4805,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^3.2.0", @@ -4567,7 +4819,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/csstype": { @@ -4702,7 +4954,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -4770,7 +5022,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4788,7 +5040,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -4801,7 +5053,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -4860,11 +5112,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "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==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4884,7 +5142,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4902,7 +5160,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4954,7 +5212,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5036,7 +5294,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5046,7 +5304,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5056,14 +5314,14 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, + "devOptional": 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==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5076,7 +5334,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5120,7 +5378,7 @@ "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5359,7 +5617,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -5385,7 +5643,7 @@ "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, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -5433,7 +5691,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -5568,7 +5826,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5601,7 +5859,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5616,7 +5873,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5677,7 +5934,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5709,7 +5966,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5848,7 +6105,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5920,7 +6177,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5933,7 +6190,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5949,7 +6206,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5979,7 +6236,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -5992,7 +6249,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -6006,7 +6263,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -6020,7 +6277,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6375,7 +6632,7 @@ "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, + "devOptional": true, "license": "MIT" }, "node_modules/is-regex": { @@ -6598,12 +6855,21 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6627,7 +6893,7 @@ "version": "25.0.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cssstyle": "^4.1.0", @@ -6751,6 +7017,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kysely": { + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz", + "integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6779,7 +7054,7 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "dev": true, + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -6812,7 +7087,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6833,7 +7107,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6854,7 +7127,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6875,7 +7147,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6896,7 +7167,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6917,7 +7187,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6938,7 +7207,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6959,7 +7227,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6980,7 +7247,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7001,7 +7267,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7022,7 +7287,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7096,7 +7360,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lru-cache": { @@ -7124,7 +7388,7 @@ "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, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -7134,7 +7398,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7144,7 +7408,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7154,7 +7418,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -7200,14 +7464,14 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -7222,6 +7486,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz", + "integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7240,7 +7519,7 @@ "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-inspect": { @@ -7379,7 +7658,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -7446,14 +7725,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/pathval": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.16" @@ -7463,14 +7742,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -7493,7 +7772,7 @@ "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -7575,7 +7854,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -7906,7 +8185,7 @@ "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -7947,11 +8226,17 @@ "fsevents": "~2.3.2" } }, + "node_modules/rou3": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", + "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", + "license": "MIT" + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/safe-array-concat": { @@ -8034,14 +8319,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -8237,7 +8522,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/signal-exit": { @@ -8281,7 +8566,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8291,7 +8576,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -8302,7 +8587,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8349,14 +8634,14 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { @@ -8515,7 +8800,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "js-tokens": "^9.0.1" @@ -8528,7 +8813,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/supports-color": { @@ -8561,7 +8846,7 @@ "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, + "devOptional": true, "license": "MIT" }, "node_modules/tailwindcss": { @@ -8618,7 +8903,7 @@ "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -8643,21 +8928,21 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -8674,7 +8959,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -8684,7 +8969,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -8694,7 +8979,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -8704,7 +8989,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tldts-core": "^6.1.86" @@ -8717,14 +9002,14 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" @@ -8737,7 +9022,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -8924,7 +9209,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -9081,7 +9366,7 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -9156,7 +9441,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", @@ -9210,7 +9495,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", @@ -9283,7 +9568,7 @@ "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, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -9296,7 +9581,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -9307,7 +9592,7 @@ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -9320,7 +9605,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -9330,7 +9615,7 @@ "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^5.1.0", @@ -9449,7 +9734,7 @@ "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, + "devOptional": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", @@ -9847,7 +10132,7 @@ "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9869,7 +10154,7 @@ "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, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -9879,7 +10164,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/yallist": { @@ -9906,7 +10191,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index cbab402..67e3891 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.0.0", + "better-auth": "^1.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.0", diff --git a/src/App.test.tsx b/src/App.test.tsx index 00a2593..27e040d 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,7 +1,13 @@ import { render, screen } from '@testing-library/react' -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import App from './App.tsx' +vi.mock('./lib/auth-client.ts', () => ({ + authClient: { + useSession: () => ({ data: null, isPending: false }), + }, +})) + describe('App', () => { it('renders the dashboard on the root route', () => { render() diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index b6bb0fa..6c3df87 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,10 +1,25 @@ +import { useEffect } from 'react' import { Navigate, Outlet } from 'react-router-dom' +import { authClient } from '../lib/auth-client.ts' import { useAuthStore } from '../stores/auth.ts' export function ProtectedRoute() { - const isAuthenticated = useAuthStore((s) => s.isAuthenticated) + const { data: session, isPending } = authClient.useSession() + const setAuthenticated = useAuthStore((s) => s.setAuthenticated) - if (!isAuthenticated) { + useEffect(() => { + setAuthenticated(!!session) + }, [session, setAuthenticated]) + + if (isPending) { + return ( +
+
+
+ ) + } + + if (!session) { return } diff --git a/src/lib/api.ts b/src/lib/api.ts index beaced7..3907dde 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,100 +1,98 @@ -import { useAuthStore } from '../stores/auth.ts' -import { - mockPurchases, - mockProducts, - mockCoupons, - mockAlerts, - getMockPriceHistory, -} from './mock-data.ts' - -const API_BASE = import.meta.env.VITE_API_URL ?? '/api/v1' -const USE_MOCK = import.meta.env.VITE_MOCK_API === 'true' - -// Mock response lookup table -const mockRoutes: Record unknown> = { - '/purchases': () => mockPurchases, - '/products': () => mockProducts, - '/coupons': () => mockCoupons, - '/price-alerts': () => mockAlerts, -} - -function matchMockRoute(path: string): T | null { - // Exact match - if (mockRoutes[path]) return mockRoutes[path](path) as T - - // /purchases/:id - const purchaseMatch = path.match(/^\/purchases\/(.+)$/) - if (purchaseMatch) { - const purchase = mockPurchases.find((p) => p.id === purchaseMatch[1]) - return (purchase ?? null) as T - } - - // /products/:id/price-history - const priceHistoryMatch = path.match(/^\/products\/(.+)\/price-history$/) - if (priceHistoryMatch) { - return getMockPriceHistory(priceHistoryMatch[1]) as T - } - - // /products?q=search or /products/:id - const productMatch = path.match(/^\/products\/(.+)$/) - if (productMatch) { - const product = mockProducts.find((p) => p.id === productMatch[1]) - return (product ?? null) as T - } - - const productsSearch = path.match(/^\/products\?q=(.+)$/) - if (productsSearch) { - const q = decodeURIComponent(productsSearch[1]).toLowerCase() - return mockProducts.filter( - (p) => - p.name.toLowerCase().includes(q) || - p.brand.toLowerCase().includes(q) || - p.category.toLowerCase().includes(q), - ) as T - } - - return null -} - -async function apiFetch(path: string, options?: RequestInit): Promise { - // Mock interceptor: return mock data without hitting the network - if (USE_MOCK && (!options?.method || options.method === 'GET')) { - const mockResult = matchMockRoute(path) - if (mockResult !== null) { - // Simulate network delay for realistic loading states - await new Promise((r) => setTimeout(r, 300)) - return mockResult - } - } - - const token = useAuthStore.getState().token - - const res = await fetch(`${API_BASE}${path}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...options?.headers, - }, - }) - - if (res.status === 401) { - useAuthStore.getState().logout() - throw new Error('Unauthorized') - } - - if (!res.ok) { - throw new Error(`API error: ${res.status}`) - } - - return res.json() as Promise -} - -export const api = { - get: (path: string) => apiFetch(path), - post: (path: string, body: unknown) => - apiFetch(path, { method: 'POST', body: JSON.stringify(body) }), - put: (path: string, body: unknown) => - apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }), - delete: (path: string) => apiFetch(path, { method: 'DELETE' }), -} +import { useAuthStore } from '../stores/auth.ts' +import { + mockPurchases, + mockProducts, + mockCoupons, + mockAlerts, + getMockPriceHistory, +} from './mock-data.ts' + +const API_BASE = import.meta.env.VITE_API_URL ?? '/api/v1' +const USE_MOCK = import.meta.env.VITE_MOCK_API === 'true' + +// Mock response lookup table +const mockRoutes: Record unknown> = { + '/purchases': () => mockPurchases, + '/products': () => mockProducts, + '/coupons': () => mockCoupons, + '/price-alerts': () => mockAlerts, +} + +function matchMockRoute(path: string): T | null { + // Exact match + if (mockRoutes[path]) return mockRoutes[path](path) as T + + // /purchases/:id + const purchaseMatch = path.match(/^\/purchases\/(.+)$/) + if (purchaseMatch) { + const purchase = mockPurchases.find((p) => p.id === purchaseMatch[1]) + return (purchase ?? null) as T + } + + // /products/:id/price-history + const priceHistoryMatch = path.match(/^\/products\/(.+)\/price-history$/) + if (priceHistoryMatch) { + return getMockPriceHistory(priceHistoryMatch[1]) as T + } + + // /products/:id + const productMatch = path.match(/^\/products\/(.+)$/) + if (productMatch) { + const product = mockProducts.find((p) => p.id === productMatch[1]) + return (product ?? null) as T + } + + const productsSearch = path.match(/^\/products\?q=(.+)$/) + if (productsSearch) { + const q = decodeURIComponent(productsSearch[1]).toLowerCase() + return mockProducts.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.brand.toLowerCase().includes(q) || + p.category.toLowerCase().includes(q), + ) as T + } + + return null +} + +async function apiFetch(path: string, options?: RequestInit): Promise { + // Mock interceptor: return mock data without hitting the network + if (USE_MOCK && (!options?.method || options.method === 'GET')) { + const mockResult = matchMockRoute(path) + if (mockResult !== null) { + // Simulate network delay for realistic loading states + await new Promise((r) => setTimeout(r, 300)) + return mockResult + } + } + + const res = await fetch(`${API_BASE}${path}`, { + ...options, + credentials: 'include', // Send Better-Auth session cookie + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }) + + if (res.status === 401) { + useAuthStore.getState().setAuthenticated(false) + throw new Error('Unauthorized') + } + + if (!res.ok) { + throw new Error(`API error: ${res.status}`) + } + + return res.json() as Promise +} + +export const api = { + get: (path: string) => apiFetch(path), + post: (path: string, body: unknown) => + apiFetch(path, { method: 'POST', body: JSON.stringify(body) }), + put: (path: string, body: unknown) => + apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }), + delete: (path: string) => apiFetch(path, { method: 'DELETE' }), +} diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..a6fe18f --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,8 @@ +import { createAuthClient } from "better-auth/react" + +export const authClient = createAuthClient({ + baseURL: import.meta.env.VITE_AUTH_URL ?? "http://localhost:3001", + basePath: "/auth", +}) + +export const { useSession, signIn, signUp, signOut } = authClient diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index e68c63a..d1e885f 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,197 +1,200 @@ -import React, { Suspense } from 'react' -import { Link } from 'react-router-dom' -import { useAuthStore } from '../stores/auth.ts' -import { usePurchases, usePriceAlerts, usePriceHistory } from '../hooks/useApi.ts' -import { StoreIcon } from '../components/StoreIcon.tsx' - -const LazySparklineCard = React.lazy(() => - import('../components/SparklineChart.tsx').then((mod) => ({ default: mod.SparklineCard })) -) - -export function Dashboard() { - const user = useAuthStore((s) => s.user) - const isAuthenticated = useAuthStore((s) => s.isAuthenticated) - - if (!isAuthenticated) { - return ( -
-

CartSnitch

-

Track prices. Save money.

-
- - Sign In - - - Create Account - -
-
- ) - } - - return -} - -function AuthenticatedDashboard({ userName }: { userName: string }) { - const { data: purchases = [], isLoading: purchasesLoading } = usePurchases() - const { data: alerts = [], isLoading: alertsLoading } = usePriceAlerts() - const { data: eggHistory = [] } = usePriceHistory('prod10') - const { data: milkHistory = [] } = usePriceHistory('prod1') - - const triggeredAlerts = alerts.filter((a) => a.triggered) - const watchingAlerts = alerts.filter((a) => !a.triggered) - const recentPurchases = purchases.slice(0, 3) - - const sparklineData = eggHistory.filter((p) => p.storeId === 'meijer').slice(-8) - const milkSparkline = milkHistory.filter((p) => p.storeId === 'kroger').slice(-8) - - const eggCurrent = sparklineData.length > 0 ? `$${sparklineData[sparklineData.length - 1].price.toFixed(2)}` : '—' - const milkCurrent = milkSparkline.length > 0 ? `$${milkSparkline[milkSparkline.length - 1].price.toFixed(2)}` : '—' - - if (purchasesLoading || alertsLoading) { - return - } - - return ( -
-

- Hi, {userName.split(' ')[0]} -

- - {/* Triggered alerts banner */} - {triggeredAlerts.length > 0 && ( - - - ✓ - -
-

- {triggeredAlerts.length} price {triggeredAlerts.length === 1 ? 'alert' : 'alerts'} triggered! -

-

- {triggeredAlerts.map((a) => a.productName).join(', ')} -

-
- - )} - - {/* Quick stats */} -
-
-

Watching

-

{watchingAlerts.length}

-

price alerts

-
-
-

This Month

-

- ${recentPurchases.reduce((sum, p) => sum + p.total, 0).toFixed(0)} -

-

grocery spend

-
-
- - {/* Price trend sparklines */} -
-

Price Trends

-
- }> - - - -
-
- - {/* Recent purchases */} -
-
-

Recent Purchases

- - View all - -
-
- {recentPurchases.map((purchase) => ( - - -
-

{purchase.storeName}

-

- {new Date(purchase.date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - })}{' '} - · {purchase.items.length} items -

-
- - ${purchase.total.toFixed(2)} - - - ))} -
-
- - {/* Quick actions */} -
-

Quick Actions

-
- - Compare Prices - - - Link a Store - -
-
-
- ) -} - -function DashboardSkeleton() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
- ) -} - -function SparklinePlaceholder() { - return ( -
-
-
-
-
-
-
- ) -} +import React, { Suspense } from 'react' +import { Link } from 'react-router-dom' +import { authClient } from '../lib/auth-client.ts' +import { usePurchases, usePriceAlerts, usePriceHistory } from '../hooks/useApi.ts' +import { StoreIcon } from '../components/StoreIcon.tsx' + +const LazySparklineCard = React.lazy(() => + import('../components/SparklineChart.tsx').then((mod) => ({ default: mod.SparklineCard })) +) + +export function Dashboard() { + const { data: session, isPending } = authClient.useSession() + + if (isPending) { + return + } + + if (!session) { + return ( +
+

CartSnitch

+

Track prices. Save money.

+
+ + Sign In + + + Create Account + +
+
+ ) + } + + return +} + +function AuthenticatedDashboard({ userName }: { userName: string }) { + const { data: purchases = [], isLoading: purchasesLoading } = usePurchases() + const { data: alerts = [], isLoading: alertsLoading } = usePriceAlerts() + const { data: eggHistory = [] } = usePriceHistory('prod10') + const { data: milkHistory = [] } = usePriceHistory('prod1') + + const triggeredAlerts = alerts.filter((a) => a.triggered) + const watchingAlerts = alerts.filter((a) => !a.triggered) + const recentPurchases = purchases.slice(0, 3) + + const sparklineData = eggHistory.filter((p) => p.storeId === 'meijer').slice(-8) + const milkSparkline = milkHistory.filter((p) => p.storeId === 'kroger').slice(-8) + + const eggCurrent = sparklineData.length > 0 ? `$${sparklineData[sparklineData.length - 1].price.toFixed(2)}` : '—' + const milkCurrent = milkSparkline.length > 0 ? `$${milkSparkline[milkSparkline.length - 1].price.toFixed(2)}` : '—' + + if (purchasesLoading || alertsLoading) { + return + } + + return ( +
+

+ Hi, {userName.split(' ')[0]} +

+ + {/* Triggered alerts banner */} + {triggeredAlerts.length > 0 && ( + + + ✓ + +
+

+ {triggeredAlerts.length} price {triggeredAlerts.length === 1 ? 'alert' : 'alerts'} triggered! +

+

+ {triggeredAlerts.map((a) => a.productName).join(', ')} +

+
+ + )} + + {/* Quick stats */} +
+
+

Watching

+

{watchingAlerts.length}

+

price alerts

+
+
+

This Month

+

+ ${recentPurchases.reduce((sum, p) => sum + p.total, 0).toFixed(0)} +

+

grocery spend

+
+
+ + {/* Price trend sparklines */} +
+

Price Trends

+
+ }> + + + +
+
+ + {/* Recent purchases */} +
+
+

Recent Purchases

+ + View all + +
+
+ {recentPurchases.map((purchase) => ( + + +
+

{purchase.storeName}

+

+ {new Date(purchase.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + })}{' '} + · {purchase.items.length} items +

+
+ + ${purchase.total.toFixed(2)} + + + ))} +
+
+ + {/* Quick actions */} +
+

Quick Actions

+
+ + Compare Prices + + + Link a Store + +
+
+
+ ) +} + +function DashboardSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ) +} + +function SparklinePlaceholder() { + return ( +
+
+
+
+
+
+
+ ) +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index d29b0f1..214dcd4 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,92 +1,97 @@ -import { useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' -import { useAuthStore } from '../stores/auth.ts' -import { api } from '../lib/api.ts' -import { mockUser } from '../lib/mock-data.ts' -import type { User } from '../types/api.ts' - -export function Login() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - const navigate = useNavigate() - const setAuth = useAuthStore((s) => s.setAuth) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError('') - - if (!email || !password) { - setError('Please fill in all fields.') - return - } - - setLoading(true) - try { - const res = await api.post<{ user: User; token: string }>('/auth/login', { email, password }) - setAuth(res.user, res.token) - navigate('/') - } catch { - if (import.meta.env.VITE_MOCK_AUTH === 'true') { - // Fallback to mock auth for demo - setAuth(mockUser, 'mock-jwt-token') - navigate('/') - } else { - setError('Invalid email or password. Please try again.') - } - } finally { - setLoading(false) - } - } - - return ( -
-

CartSnitch

-

Track prices. Save money.

- - {error && ( -
- {error} -
- )} - -
- setEmail(e.target.value)} - autoComplete="email" - className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" - /> - setPassword(e.target.value)} - autoComplete="current-password" - className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" - /> - -
- - - Forgot password? - - -

- Don't have an account?{' '} - - Sign up - -

-
- ) -} +import { useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { authClient } from '../lib/auth-client.ts' +import { useAuthStore } from '../stores/auth.ts' + +export function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + const navigate = useNavigate() + const setAuthenticated = useAuthStore((s) => s.setAuthenticated) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + + if (!email || !password) { + setError('Please fill in all fields.') + return + } + + setLoading(true) + try { + const { error: authError } = await authClient.signIn.email({ + email, + password, + }) + + if (authError) { + throw new Error(authError.message ?? 'Sign in failed') + } + + setAuthenticated(true) + navigate('/') + } catch { + if (import.meta.env.VITE_MOCK_AUTH === 'true') { + setAuthenticated(true) + navigate('/') + } else { + setError('Invalid email or password. Please try again.') + } + } finally { + setLoading(false) + } + } + + return ( +
+

CartSnitch

+

Track prices. Save money.

+ + {error && ( +
+ {error} +
+ )} + +
+ setEmail(e.target.value)} + autoComplete="email" + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + setPassword(e.target.value)} + autoComplete="current-password" + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + +
+ + + Forgot password? + + +

+ Don't have an account?{' '} + + Sign up + +

+
+ ) +} diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 49bcffc..a65e7b6 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,102 +1,108 @@ -import { useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' -import { useAuthStore } from '../stores/auth.ts' -import { api } from '../lib/api.ts' -import { mockUser } from '../lib/mock-data.ts' -import type { User } from '../types/api.ts' - -export function Register() { - const [name, setName] = useState('') - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - const navigate = useNavigate() - const setAuth = useAuthStore((s) => s.setAuth) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError('') - - if (!name || !email || !password) { - setError('Please fill in all fields.') - return - } - - if (password.length < 8) { - setError('Password must be at least 8 characters.') - return - } - - setLoading(true) - try { - const res = await api.post<{ user: User; token: string }>('/auth/register', { name, email, password }) - setAuth(res.user, res.token) - navigate('/') - } catch { - if (import.meta.env.VITE_MOCK_AUTH === 'true') { - // Fallback to mock auth for demo - setAuth({ ...mockUser, name, email }, 'mock-jwt-token') - navigate('/') - } else { - setError('Registration failed. Please try again.') - } - } finally { - setLoading(false) - } - } - - return ( -
-

Create Account

-

Start tracking your grocery prices.

- - {error && ( -
- {error} -
- )} - -
- setName(e.target.value)} - autoComplete="name" - className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" - /> - setEmail(e.target.value)} - autoComplete="email" - className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" - /> - setPassword(e.target.value)} - autoComplete="new-password" - className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" - /> - -
- -

- Already have an account?{' '} - - Sign in - -

-
- ) -} +import { useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { authClient } from '../lib/auth-client.ts' +import { useAuthStore } from '../stores/auth.ts' + +export function Register() { + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + const navigate = useNavigate() + const setAuthenticated = useAuthStore((s) => s.setAuthenticated) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + + if (!name || !email || !password) { + setError('Please fill in all fields.') + return + } + + if (password.length < 8) { + setError('Password must be at least 8 characters.') + return + } + + setLoading(true) + try { + const { error: authError } = await authClient.signUp.email({ + name, + email, + password, + }) + + if (authError) { + throw new Error(authError.message ?? 'Registration failed') + } + + setAuthenticated(true) + navigate('/') + } catch { + if (import.meta.env.VITE_MOCK_AUTH === 'true') { + setAuthenticated(true) + navigate('/') + } else { + setError('Registration failed. Please try again.') + } + } finally { + setLoading(false) + } + } + + return ( +
+

Create Account

+

Start tracking your grocery prices.

+ + {error && ( +
+ {error} +
+ )} + +
+ setName(e.target.value)} + autoComplete="name" + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + setEmail(e.target.value)} + autoComplete="email" + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + setPassword(e.target.value)} + autoComplete="new-password" + className="min-h-12 w-full rounded-xl border border-gray-200 px-4 text-base focus:border-brand-blue focus:outline-none focus:ring-1 focus:ring-brand-blue" + /> + +
+ +

+ Already have an account?{' '} + + Sign in + +

+
+ ) +} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 43e5d2d..5ad0382 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,18 +1,21 @@ import { Link, useNavigate } from 'react-router-dom' +import { authClient } from '../lib/auth-client.ts' import { useAuthStore } from '../stores/auth.ts' import { useThemeStore } from '../stores/theme.ts' import { StoreIcon } from '../components/StoreIcon.tsx' export function Settings() { - const user = useAuthStore((s) => s.user) - const logout = useAuthStore((s) => s.logout) + const { data: session } = authClient.useSession() + const setAuthenticated = useAuthStore((s) => s.setAuthenticated) const navigate = useNavigate() const { theme, setTheme } = useThemeStore() - const connectedStores = user?.connectedStores ?? [] + const user = session?.user + const connectedStores: string[] = [] - function handleSignOut() { - logout() + async function handleSignOut() { + await authClient.signOut() + setAuthenticated(false) navigate('/login') } diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 1ffd8b8..a9abb5d 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,27 +1,18 @@ -import { create } from 'zustand' -import { persist } from 'zustand/middleware' -import type { User } from '../types/api.ts' - -interface AuthState { - user: User | null - token: string | null - isAuthenticated: boolean - setAuth: (user: User, token: string) => void - logout: () => void -} - -export const useAuthStore = create()( - persist( - (set) => ({ - user: null, - token: null, - isAuthenticated: false, - setAuth: (user, token) => set({ user, token, isAuthenticated: true }), - logout: () => set({ user: null, token: null, isAuthenticated: false }), - }), - { - name: 'cartsnitch-auth', - partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }), - }, - ), -) +import { create } from 'zustand' + +/** + * Minimal auth state for UI reactivity. + * + * Session management is handled by Better-Auth via httpOnly cookies. + * This store only tracks whether we have an active session for UI + * gating (protected routes, nav state). No tokens in memory or localStorage. + */ +interface AuthState { + isAuthenticated: boolean + setAuthenticated: (value: boolean) => void +} + +export const useAuthStore = create()((set) => ({ + isAuthenticated: false, + setAuthenticated: (value) => set({ isAuthenticated: value }), +}))