diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 7119fcb6d..491df0992 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -57,7 +57,7 @@ jobs: working-directory: surfsense_web env: NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${{ vars.NEXT_PUBLIC_FASTAPI_BACKEND_URL }} - NEXT_PUBLIC_ELECTRIC_URL: ${{ vars.NEXT_PUBLIC_ELECTRIC_URL }} + NEXT_PUBLIC_ZERO_CACHE_URL: ${{ vars.NEXT_PUBLIC_ZERO_CACHE_URL }} NEXT_PUBLIC_DEPLOYMENT_MODE: ${{ vars.NEXT_PUBLIC_DEPLOYMENT_MODE }} NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${{ vars.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE }} diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a53a4b414..2e5de8cc6 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -164,8 +164,7 @@ jobs: ${{ matrix.image == 'web' && 'NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__' || '' }} ${{ matrix.image == 'web' && 'NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__' || '' }} ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__' || '' }} - ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ELECTRIC_URL=__NEXT_PUBLIC_ELECTRIC_URL__' || '' }} - ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ELECTRIC_AUTH_MODE=__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__' || '' }} + ${{ matrix.image == 'web' && 'NEXT_PUBLIC_ZERO_CACHE_URL=__NEXT_PUBLIC_ZERO_CACHE_URL__' || '' }} ${{ matrix.image == 'web' && 'NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__' || '' }} - name: Export digest diff --git a/docker/.env.example b/docker/.env.example index a226c2624..766f92dcc 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -35,7 +35,7 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # BACKEND_PORT=8929 # FRONTEND_PORT=3929 -# ELECTRIC_PORT=5929 +# ZERO_CACHE_PORT=5929 # SEARXNG_PORT=8888 # FLOWER_PORT=5555 @@ -58,7 +58,6 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL # NEXT_PUBLIC_ETL_SERVICE=DOCLING # NEXT_PUBLIC_DEPLOYMENT_MODE=self-hosted -# NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure # ------------------------------------------------------------------------------ # Custom Domain / Reverse Proxy @@ -71,8 +70,35 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # NEXT_FRONTEND_URL=https://app.yourdomain.com # BACKEND_URL=https://api.yourdomain.com # NEXT_PUBLIC_FASTAPI_BACKEND_URL=https://api.yourdomain.com -# NEXT_PUBLIC_ELECTRIC_URL=https://electric.yourdomain.com +# NEXT_PUBLIC_ZERO_CACHE_URL=https://zero.yourdomain.com +# ------------------------------------------------------------------------------ +# Zero-cache (real-time sync) +# ------------------------------------------------------------------------------ +# Defaults work out of the box for Docker deployments. +# Change ZERO_ADMIN_PASSWORD for security in production. + +# ZERO_ADMIN_PASSWORD=surfsense-zero-admin +# Full override for the Zero → Postgres connection URLs. +# Leave commented out to use the Docker-managed `db` container (default). +# ZERO_UPSTREAM_DB=postgresql://surfsense:surfsense@db:5432/surfsense +# ZERO_CVR_DB=postgresql://surfsense:surfsense@db:5432/surfsense +# ZERO_CHANGE_DB=postgresql://surfsense:surfsense@db:5432/surfsense + +# ZERO_QUERY_URL: where zero-cache forwards query requests for resolution. +# ZERO_MUTATE_URL: required by zero-cache when auth tokens are used, even though +# SurfSense does not use Zero mutators. Setting both URLs tells zero-cache to +# skip its own JWT verification and let the app endpoints handle auth instead. +# The mutate endpoint is a no-op that returns an empty response. +# Default: Docker service networking (http://frontend:3000/api/zero/...). +# Override when running the frontend outside Docker: +# ZERO_QUERY_URL=http://host.docker.internal:3000/api/zero/query +# ZERO_MUTATE_URL=http://host.docker.internal:3000/api/zero/mutate +# Override for custom domain: +# ZERO_QUERY_URL=https://app.yourdomain.com/api/zero/query +# ZERO_MUTATE_URL=https://app.yourdomain.com/api/zero/mutate +# ZERO_QUERY_URL=http://frontend:3000/api/zero/query +# ZERO_MUTATE_URL=http://frontend:3000/api/zero/mutate # ------------------------------------------------------------------------------ # Database (defaults work out of the box, change for security) @@ -101,19 +127,6 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # Supports TLS: rediss://:password@host:6380/0 # REDIS_URL=redis://redis:6379/0 -# ------------------------------------------------------------------------------ -# Electric SQL (real-time sync credentials) -# ------------------------------------------------------------------------------ -# These must match on the db, backend, and electric services. -# Change for security; defaults work out of the box. - -# ELECTRIC_DB_USER=electric -# ELECTRIC_DB_PASSWORD=electric_password -# Full override for the Electric → Postgres connection URL. -# Leave commented out to use the Docker-managed `db` container (default). -# Uncomment and set `db` to `host.docker.internal` when pointing Electric at a local Postgres instance (e.g. Postgres.app on macOS): -# ELECTRIC_DATABASE_URL=postgresql://electric:electric_password@db:5432/surfsense?sslmode=disable - # ------------------------------------------------------------------------------ # TTS & STT (Text-to-Speech / Speech-to-Text) # ------------------------------------------------------------------------------ diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 15531bf55..564ecd772 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -18,13 +18,10 @@ services: volumes: - postgres_data:/var/lib/postgresql/data - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro - - ./scripts/init-electric-user.sh:/docker-entrypoint-initdb.d/init-electric-user.sh:ro environment: - POSTGRES_USER=${DB_USER:-postgres} - POSTGRES_PASSWORD=${DB_PASSWORD:-postgres} - POSTGRES_DB=${DB_NAME:-surfsense} - - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric} - - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password} command: postgres -c config_file=/etc/postgresql/postgresql.conf healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-surfsense}"] @@ -91,8 +88,6 @@ services: - UNSTRUCTURED_HAS_PATCHED_LOOP=1 - LANGCHAIN_TRACING_V2=false - LANGSMITH_TRACING=false - - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric} - - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password} - AUTH_TYPE=${AUTH_TYPE:-LOCAL} - NEXT_FRONTEND_URL=${NEXT_FRONTEND_URL:-http://localhost:3000} - SEARXNG_DEFAULT_HOST=${SEARXNG_DEFAULT_HOST:-http://searxng:8080} @@ -130,8 +125,6 @@ services: - REDIS_APP_URL=${REDIS_URL:-redis://redis:6379/0} - CELERY_TASK_DEFAULT_QUEUE=surfsense - PYTHONPATH=/app - - ELECTRIC_DB_USER=${ELECTRIC_DB_USER:-electric} - - ELECTRIC_DB_PASSWORD=${ELECTRIC_DB_PASSWORD:-electric_password} - SEARXNG_DEFAULT_HOST=${SEARXNG_DEFAULT_HOST:-http://searxng:8080} - SERVICE_ROLE=worker depends_on: @@ -176,20 +169,28 @@ services: # - redis # - celery_worker - electric: - image: electricsql/electric:1.4.10 + zero-cache: + image: rocicorp/zero:0.26.2 ports: - - "${ELECTRIC_PORT:-5133}:3000" + - "${ZERO_CACHE_PORT:-4848}:4848" + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: db: condition: service_healthy environment: - - DATABASE_URL=${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} - - ELECTRIC_INSECURE=true - - ELECTRIC_WRITE_TO_PG_MODE=direct + - ZERO_UPSTREAM_DB=${ZERO_UPSTREAM_DB:-postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} + - ZERO_CVR_DB=${ZERO_CVR_DB:-postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} + - ZERO_CHANGE_DB=${ZERO_CHANGE_DB:-postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} + - ZERO_REPLICA_FILE=/data/zero.db + - ZERO_ADMIN_PASSWORD=${ZERO_ADMIN_PASSWORD:-surfsense-zero-admin} + - ZERO_QUERY_URL=${ZERO_QUERY_URL:-http://frontend:3000/api/zero/query} + - ZERO_MUTATE_URL=${ZERO_MUTATE_URL:-http://frontend:3000/api/zero/mutate} + volumes: + - zero_cache_data:/data restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"] + test: ["CMD", "curl", "-f", "http://localhost:4848/keepalive"] interval: 10s timeout: 5s retries: 5 @@ -201,8 +202,7 @@ services: NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000} NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL} NEXT_PUBLIC_ETL_SERVICE: ${NEXT_PUBLIC_ETL_SERVICE:-DOCLING} - NEXT_PUBLIC_ELECTRIC_URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:5133} - NEXT_PUBLIC_ELECTRIC_AUTH_MODE: ${NEXT_PUBLIC_ELECTRIC_AUTH_MODE:-insecure} + NEXT_PUBLIC_ZERO_CACHE_URL: ${NEXT_PUBLIC_ZERO_CACHE_URL:-http://localhost:${ZERO_CACHE_PORT:-4848}} NEXT_PUBLIC_DEPLOYMENT_MODE: ${NEXT_PUBLIC_DEPLOYMENT_MODE:-self-hosted} ports: - "${FRONTEND_PORT:-3000}:3000" @@ -211,7 +211,7 @@ services: depends_on: backend: condition: service_healthy - electric: + zero-cache: condition: service_healthy volumes: @@ -223,3 +223,5 @@ volumes: name: surfsense-dev-redis shared_temp: name: surfsense-dev-shared-temp + zero_cache_data: + name: surfsense-dev-zero-cache diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8c85248d2..b03efdd2f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -15,13 +15,10 @@ services: volumes: - postgres_data:/var/lib/postgresql/data - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro - - ./scripts/init-electric-user.sh:/docker-entrypoint-initdb.d/init-electric-user.sh:ro environment: POSTGRES_USER: ${DB_USER:-surfsense} POSTGRES_PASSWORD: ${DB_PASSWORD:-surfsense} POSTGRES_DB: ${DB_NAME:-surfsense} - ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric} - ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password} command: postgres -c config_file=/etc/postgresql/postgresql.conf restart: unless-stopped healthcheck: @@ -72,8 +69,6 @@ services: PYTHONPATH: /app UVICORN_LOOP: asyncio UNSTRUCTURED_HAS_PATCHED_LOOP: "1" - ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric} - ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password} NEXT_FRONTEND_URL: ${NEXT_FRONTEND_URL:-http://localhost:${FRONTEND_PORT:-3929}} SEARXNG_DEFAULT_HOST: ${SEARXNG_DEFAULT_HOST:-http://searxng:8080} # Daytona Sandbox – uncomment and set credentials to enable cloud code execution @@ -112,8 +107,6 @@ services: REDIS_APP_URL: ${REDIS_URL:-redis://redis:6379/0} CELERY_TASK_DEFAULT_QUEUE: surfsense PYTHONPATH: /app - ELECTRIC_DB_USER: ${ELECTRIC_DB_USER:-electric} - ELECTRIC_DB_PASSWORD: ${ELECTRIC_DB_PASSWORD:-electric_password} SEARXNG_DEFAULT_HOST: ${SEARXNG_DEFAULT_HOST:-http://searxng:8080} SERVICE_ROLE: worker depends_on: @@ -165,20 +158,28 @@ services: # - celery_worker # restart: unless-stopped - electric: - image: electricsql/electric:1.4.10 + zero-cache: + image: rocicorp/zero:0.26.2 ports: - - "${ELECTRIC_PORT:-5929}:3000" + - "${ZERO_CACHE_PORT:-5929}:4848" + extra_hosts: + - "host.docker.internal:host-gateway" environment: - DATABASE_URL: ${ELECTRIC_DATABASE_URL:-postgresql://${ELECTRIC_DB_USER:-electric}:${ELECTRIC_DB_PASSWORD:-electric_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} - ELECTRIC_INSECURE: "true" - ELECTRIC_WRITE_TO_PG_MODE: direct + ZERO_UPSTREAM_DB: ${ZERO_UPSTREAM_DB:-postgresql://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} + ZERO_CVR_DB: ${ZERO_CVR_DB:-postgresql://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} + ZERO_CHANGE_DB: ${ZERO_CHANGE_DB:-postgresql://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}?sslmode=${DB_SSLMODE:-disable}} + ZERO_REPLICA_FILE: /data/zero.db + ZERO_ADMIN_PASSWORD: ${ZERO_ADMIN_PASSWORD:-surfsense-zero-admin} + ZERO_QUERY_URL: ${ZERO_QUERY_URL:-http://frontend:3000/api/zero/query} + ZERO_MUTATE_URL: ${ZERO_MUTATE_URL:-http://frontend:3000/api/zero/mutate} + volumes: + - zero_cache_data:/data restart: unless-stopped depends_on: db: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"] + test: ["CMD", "curl", "-f", "http://localhost:4848/keepalive"] interval: 10s timeout: 5s retries: 5 @@ -189,17 +190,16 @@ services: - "${FRONTEND_PORT:-3929}:3000" environment: NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8929}} - NEXT_PUBLIC_ELECTRIC_URL: ${NEXT_PUBLIC_ELECTRIC_URL:-http://localhost:${ELECTRIC_PORT:-5929}} + NEXT_PUBLIC_ZERO_CACHE_URL: ${NEXT_PUBLIC_ZERO_CACHE_URL:-http://localhost:${ZERO_CACHE_PORT:-5929}} NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL} NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING} NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted} - NEXT_PUBLIC_ELECTRIC_AUTH_MODE: ${NEXT_PUBLIC_ELECTRIC_AUTH_MODE:-insecure} labels: - "com.centurylinklabs.watchtower.enable=true" depends_on: backend: condition: service_healthy - electric: + zero-cache: condition: service_healthy restart: unless-stopped @@ -210,3 +210,5 @@ volumes: name: surfsense-redis shared_temp: name: surfsense-shared-temp + zero_cache_data: + name: surfsense-zero-cache diff --git a/docker/postgresql.conf b/docker/postgresql.conf index 99b29ba30..d0936dce8 100644 --- a/docker/postgresql.conf +++ b/docker/postgresql.conf @@ -1,11 +1,11 @@ -# PostgreSQL configuration for Electric SQL +# PostgreSQL configuration for SurfSense # This file is mounted into the PostgreSQL container listen_addresses = '*' max_connections = 200 shared_buffers = 256MB -# Enable logical replication (required for Electric SQL) +# Enable logical replication (required for Zero-cache real-time sync) wal_level = logical max_replication_slots = 10 max_wal_senders = 10 diff --git a/docker/scripts/init-electric-user.sh b/docker/scripts/init-electric-user.sh deleted file mode 100755 index fbd1c361a..000000000 --- a/docker/scripts/init-electric-user.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh -# Creates the Electric SQL replication user on first DB initialization. -# Idempotent — safe to run alongside Alembic migration 66. - -set -e - -ELECTRIC_DB_USER="${ELECTRIC_DB_USER:-electric}" -ELECTRIC_DB_PASSWORD="${ELECTRIC_DB_PASSWORD:-electric_password}" - -echo "Creating Electric SQL replication user: $ELECTRIC_DB_USER" - -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - DO \$\$ - BEGIN - IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$ELECTRIC_DB_USER') THEN - CREATE USER $ELECTRIC_DB_USER WITH REPLICATION PASSWORD '$ELECTRIC_DB_PASSWORD'; - END IF; - END - \$\$; - - GRANT CONNECT ON DATABASE $POSTGRES_DB TO $ELECTRIC_DB_USER; - GRANT CREATE ON DATABASE $POSTGRES_DB TO $ELECTRIC_DB_USER; - GRANT USAGE ON SCHEMA public TO $ELECTRIC_DB_USER; - GRANT SELECT ON ALL TABLES IN SCHEMA public TO $ELECTRIC_DB_USER; - GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO $ELECTRIC_DB_USER; - ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO $ELECTRIC_DB_USER; - ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO $ELECTRIC_DB_USER; - - DO \$\$ - BEGIN - IF NOT EXISTS (SELECT FROM pg_publication WHERE pubname = 'electric_publication_default') THEN - CREATE PUBLICATION electric_publication_default; - END IF; - END - \$\$; -EOSQL - -echo "Electric SQL user '$ELECTRIC_DB_USER' and publication created successfully" diff --git a/docker/scripts/install.ps1 b/docker/scripts/install.ps1 index b7004bae2..0eb3886a2 100644 --- a/docker/scripts/install.ps1 +++ b/docker/scripts/install.ps1 @@ -109,7 +109,6 @@ $Files = @( @{ Src = "docker/docker-compose.yml"; Dest = "docker-compose.yml" } @{ Src = "docker/.env.example"; Dest = ".env.example" } @{ Src = "docker/postgresql.conf"; Dest = "postgresql.conf" } - @{ Src = "docker/scripts/init-electric-user.sh"; Dest = "scripts/init-electric-user.sh" } @{ Src = "docker/scripts/migrate-database.ps1"; Dest = "scripts/migrate-database.ps1" } @{ Src = "docker/searxng/settings.yml"; Dest = "searxng/settings.yml" } @{ Src = "docker/searxng/limiter.toml"; Dest = "searxng/limiter.toml" } diff --git a/docker/scripts/install.sh b/docker/scripts/install.sh index 7a68a9bd1..fcab4d55a 100644 --- a/docker/scripts/install.sh +++ b/docker/scripts/install.sh @@ -108,7 +108,6 @@ FILES=( "docker/docker-compose.yml:docker-compose.yml" "docker/.env.example:.env.example" "docker/postgresql.conf:postgresql.conf" - "docker/scripts/init-electric-user.sh:scripts/init-electric-user.sh" "docker/scripts/migrate-database.sh:scripts/migrate-database.sh" "docker/searxng/settings.yml:searxng/settings.yml" "docker/searxng/limiter.toml:searxng/limiter.toml" @@ -122,7 +121,6 @@ for entry in "${FILES[@]}"; do || error "Failed to download ${dest}. Check your internet connection and try again." done -chmod +x "${INSTALL_DIR}/scripts/init-electric-user.sh" chmod +x "${INSTALL_DIR}/scripts/migrate-database.sh" success "All files downloaded to ${INSTALL_DIR}/" diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index d3717778b..94d5c8c9b 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -17,10 +17,6 @@ REDIS_APP_URL=redis://localhost:6379/0 # Only uncomment if running the backend outside Docker (e.g. uvicorn on host). # SEARXNG_DEFAULT_HOST=http://localhost:8888 -#Electric(for migrations only) -ELECTRIC_DB_USER=electric -ELECTRIC_DB_PASSWORD=electric_password - # Periodic task interval # # Run every minute (default) # SCHEDULE_CHECKER_INTERVAL=1m diff --git a/surfsense_backend/alembic/env.py b/surfsense_backend/alembic/env.py index fa213121c..bd8c20356 100644 --- a/surfsense_backend/alembic/env.py +++ b/surfsense_backend/alembic/env.py @@ -25,13 +25,6 @@ database_url = os.getenv("DATABASE_URL") if database_url: config.set_main_option("sqlalchemy.url", database_url) -# Electric SQL user credentials - centralized configuration for migrations -# These are used by migrations that set up Electric SQL replication -config.set_main_option("electric_db_user", os.getenv("ELECTRIC_DB_USER", "electric")) -config.set_main_option( - "electric_db_password", os.getenv("ELECTRIC_DB_PASSWORD", "electric_password") -) - # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: diff --git a/surfsense_backend/alembic/versions/104_add_notification_composite_indexes.py b/surfsense_backend/alembic/versions/104_add_notification_composite_indexes.py index 69e97eb0d..c3afb58d0 100644 --- a/surfsense_backend/alembic/versions/104_add_notification_composite_indexes.py +++ b/surfsense_backend/alembic/versions/104_add_notification_composite_indexes.py @@ -30,21 +30,25 @@ def upgrade() -> None: "ix_notifications_user_read_type_created", "notifications", ["user_id", "read", "type", "created_at"], + if_not_exists=True, ) op.create_index( "ix_notifications_user_space_created", "notifications", ["user_id", "search_space_id", "created_at"], + if_not_exists=True, ) op.create_index( "ix_notifications_type", "notifications", ["type"], + if_not_exists=True, ) op.create_index( "ix_notifications_search_space_id", "notifications", ["search_space_id"], + if_not_exists=True, ) diff --git a/surfsense_backend/alembic/versions/107_add_video_presentations_table.py b/surfsense_backend/alembic/versions/107_add_video_presentations_table.py index 76cd42a23..1dbfb63de 100644 --- a/surfsense_backend/alembic/versions/107_add_video_presentations_table.py +++ b/surfsense_backend/alembic/versions/107_add_video_presentations_table.py @@ -35,52 +35,60 @@ def upgrade() -> None: END $$; """) - op.create_table( - "video_presentations", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("title", sa.String(length=500), nullable=False), - sa.Column("slides", JSONB(), nullable=True), - sa.Column("scene_codes", JSONB(), nullable=True), - sa.Column( - "status", - video_presentation_status_enum, - server_default="ready", - nullable=False, - ), - sa.Column("search_space_id", sa.Integer(), nullable=False), - sa.Column("thread_id", sa.Integer(), nullable=True), - sa.Column( - "created_at", - sa.TIMESTAMP(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.ForeignKeyConstraint( - ["search_space_id"], - ["searchspaces.id"], - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["thread_id"], - ["new_chat_threads.id"], - ondelete="SET NULL", - ), - sa.PrimaryKeyConstraint("id"), + conn = op.get_bind() + result = conn.execute( + sa.text("SELECT 1 FROM information_schema.tables WHERE table_name = 'video_presentations'") ) + if not result.fetchone(): + op.create_table( + "video_presentations", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("title", sa.String(length=500), nullable=False), + sa.Column("slides", JSONB(), nullable=True), + sa.Column("scene_codes", JSONB(), nullable=True), + sa.Column( + "status", + video_presentation_status_enum, + server_default="ready", + nullable=False, + ), + sa.Column("search_space_id", sa.Integer(), nullable=False), + sa.Column("thread_id", sa.Integer(), nullable=True), + sa.Column( + "created_at", + sa.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.ForeignKeyConstraint( + ["search_space_id"], + ["searchspaces.id"], + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["thread_id"], + ["new_chat_threads.id"], + ondelete="SET NULL", + ), + sa.PrimaryKeyConstraint("id"), + ) op.create_index( "ix_video_presentations_status", "video_presentations", ["status"], + if_not_exists=True, ) op.create_index( "ix_video_presentations_thread_id", "video_presentations", ["thread_id"], + if_not_exists=True, ) op.create_index( "ix_video_presentations_created_at", "video_presentations", ["created_at"], + if_not_exists=True, ) diff --git a/surfsense_backend/alembic/versions/108_cleanup_electric_sql_artifacts.py b/surfsense_backend/alembic/versions/108_cleanup_electric_sql_artifacts.py new file mode 100644 index 000000000..0f60a8bca --- /dev/null +++ b/surfsense_backend/alembic/versions/108_cleanup_electric_sql_artifacts.py @@ -0,0 +1,104 @@ +"""Clean up Electric SQL artifacts (user, publication, replication slots) + +Revision ID: 108 +Revises: 107 + +Removes leftover Electric SQL infrastructure that is no longer needed after +the migration to Rocicorp Zero. Fully idempotent — safe on databases that +never had Electric SQL set up (fresh installs). + +Cleaned up: +- Replication slots containing 'electric' (prevents unbounded WAL growth) +- The 'electric_publication_default' publication +- Default privileges, grants, and the 'electric' database user +""" + +from collections.abc import Sequence + +from alembic import op + +revision: str = "108" +down_revision: str | None = "107" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.execute( + """ + DO $$ + DECLARE + slot RECORD; + BEGIN + -- 1. Drop inactive Electric replication slots (prevents WAL growth) + FOR slot IN + SELECT slot_name FROM pg_replication_slots + WHERE slot_name LIKE '%electric%' AND active = false + LOOP + BEGIN + PERFORM pg_drop_replication_slot(slot.slot_name); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Could not drop replication slot %: %', slot.slot_name, SQLERRM; + END; + END LOOP; + + -- Warn about active Electric slots that cannot be safely dropped + FOR slot IN + SELECT slot_name FROM pg_replication_slots + WHERE slot_name LIKE '%electric%' AND active = true + LOOP + RAISE WARNING 'Active Electric replication slot "%" was not dropped — drop it manually to stop WAL growth', slot.slot_name; + END LOOP; + + -- 2. Drop the Electric publication + BEGIN + IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'electric_publication_default') THEN + DROP PUBLICATION electric_publication_default; + END IF; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Could not drop publication electric_publication_default: %', SQLERRM; + END; + + -- 3. Revoke privileges and drop the Electric user + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'electric') THEN + BEGIN + ALTER DEFAULT PRIVILEGES IN SCHEMA public + REVOKE SELECT ON TABLES FROM electric; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + REVOKE SELECT ON SEQUENCES FROM electric; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Could not revoke default privileges from electric: %', SQLERRM; + END; + + BEGIN + REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM electric; + REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM electric; + REVOKE USAGE ON SCHEMA public FROM electric; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Could not revoke schema privileges from electric: %', SQLERRM; + END; + + BEGIN + EXECUTE format( + 'REVOKE CONNECT ON DATABASE %I FROM electric', + current_database() + ); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Could not revoke CONNECT from electric: %', SQLERRM; + END; + + BEGIN + REASSIGN OWNED BY electric TO CURRENT_USER; + DROP ROLE electric; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Could not drop role electric: %', SQLERRM; + END; + END IF; + END + $$; + """ + ) + + +def downgrade() -> None: + pass diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index ff8bf442d..132bd8dae 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -722,7 +722,7 @@ class ChatComment(BaseModel, TimestampMixin): nullable=False, index=True, ) - # Denormalized thread_id for efficient Electric SQL subscriptions (one per thread) + # Denormalized thread_id for efficient Zero subscriptions (one per thread) thread_id = Column( Integer, ForeignKey("new_chat_threads.id", ondelete="CASCADE"), @@ -792,7 +792,7 @@ class ChatCommentMention(BaseModel, TimestampMixin): class ChatSessionState(BaseModel): """ Tracks real-time session state for shared chat collaboration. - One record per thread, synced via Electric SQL. + One record per thread, synced via Zero. """ __tablename__ = "chat_session_state" diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 66471b0ed..f6975b69d 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -80,7 +80,7 @@ router.include_router(model_list_router) # Dynamic LLM model catalogue from Ope router.include_router(logs_router) router.include_router(circleback_webhook_router) # Circleback meeting webhooks router.include_router(surfsense_docs_router) # Surfsense documentation for citations -router.include_router(notifications_router) # Notifications with Electric SQL sync +router.include_router(notifications_router) # Notifications with Zero sync router.include_router(composio_router) # Composio OAuth and toolkit management router.include_router(public_chat_router) # Public chat sharing and cloning router.include_router(incentive_tasks_router) # Incentive tasks for earning free pages diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py index 2dfe1b530..503f2cf32 100644 --- a/surfsense_backend/app/routes/documents_routes.py +++ b/surfsense_backend/app/routes/documents_routes.py @@ -128,7 +128,7 @@ async def create_documents_file_upload( Upload files as documents with real-time status tracking. Implements 2-phase document status updates for real-time UI feedback: - - Phase 1: Create all documents with 'pending' status (visible in UI immediately via ElectricSQL) + - Phase 1: Create all documents with 'pending' status (visible in UI immediately via Zero) - Phase 2: Celery processes each file: pending → processing → ready/failed Requires DOCUMENTS_CREATE permission. diff --git a/surfsense_backend/app/routes/notifications_routes.py b/surfsense_backend/app/routes/notifications_routes.py index 82c267c9d..611227795 100644 --- a/surfsense_backend/app/routes/notifications_routes.py +++ b/surfsense_backend/app/routes/notifications_routes.py @@ -1,7 +1,7 @@ """ Notifications API routes. These endpoints allow marking notifications as read and fetching older notifications. -Electric SQL automatically syncs the changes to all connected clients for recent items. +Zero automatically syncs the changes to all connected clients for recent items. For older items (beyond the sync window), use the list endpoint. """ @@ -267,7 +267,7 @@ async def get_unread_count( This allows the frontend to calculate: - older_unread = total_unread - recent_unread (static until reconciliation) - - Display count = older_unread + live_recent_count (from Electric SQL) + - Display count = older_unread + live_recent_count (from Zero) """ # Calculate cutoff date for sync window cutoff_date = datetime.now(UTC) - timedelta(days=SYNC_WINDOW_DAYS) @@ -344,7 +344,7 @@ async def list_notifications( List notifications for the current user with pagination. This endpoint is used as a fallback for older notifications that are - outside the Electric SQL sync window (2 weeks). + outside the Zero sync window (2 weeks). Use `before_date` to paginate through older notifications efficiently. """ @@ -487,7 +487,7 @@ async def mark_notification_as_read( """ Mark a single notification as read. - Electric SQL will automatically sync this change to all connected clients. + Zero will automatically sync this change to all connected clients. """ # Verify the notification belongs to the user result = await session.execute( @@ -528,7 +528,7 @@ async def mark_all_notifications_as_read( """ Mark all notifications as read for the current user. - Electric SQL will automatically sync these changes to all connected clients. + Zero will automatically sync these changes to all connected clients. """ # Update all unread notifications for the user result = await session.execute( diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index c12621f60..1ffc6341f 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -1543,7 +1543,7 @@ async def _run_indexing_with_notifications( ) await ( session.commit() - ) # Commit to ensure Electric SQL syncs the notification update + ) # Commit to ensure Zero syncs the notification update elif documents_processed > 0: # Update notification to storing stage if notification: @@ -1570,7 +1570,7 @@ async def _run_indexing_with_notifications( ) await ( session.commit() - ) # Commit to ensure Electric SQL syncs the notification update + ) # Commit to ensure Zero syncs the notification update else: # No new documents processed - check if this is an error or just no changes if error_or_warning: @@ -1596,7 +1596,7 @@ async def _run_indexing_with_notifications( if is_duplicate_warning or is_empty_result or is_info_warning: # These are success cases - sync worked, just found nothing new logger.info(f"Indexing completed successfully: {error_or_warning}") - # Still update timestamp so ElectricSQL syncs and clears "Syncing" UI + # Still update timestamp so Zero syncs and clears "Syncing" UI if update_timestamp_func: await update_timestamp_func(session, connector_id) await session.commit() # Commit timestamp update @@ -1619,7 +1619,7 @@ async def _run_indexing_with_notifications( ) await ( session.commit() - ) # Commit to ensure Electric SQL syncs the notification update + ) # Commit to ensure Zero syncs the notification update else: # Actual failure logger.error(f"Indexing failed: {error_or_warning}") @@ -1637,13 +1637,13 @@ async def _run_indexing_with_notifications( ) await ( session.commit() - ) # Commit to ensure Electric SQL syncs the notification update + ) # Commit to ensure Zero syncs the notification update else: # Success - just no new documents to index (all skipped/unchanged) logger.info( "Indexing completed: No new documents to process (all up to date)" ) - # Still update timestamp so ElectricSQL syncs and clears "Syncing" UI + # Still update timestamp so Zero syncs and clears "Syncing" UI if update_timestamp_func: await update_timestamp_func(session, connector_id) await session.commit() # Commit timestamp update @@ -1659,7 +1659,7 @@ async def _run_indexing_with_notifications( ) await ( session.commit() - ) # Commit to ensure Electric SQL syncs the notification update + ) # Commit to ensure Zero syncs the notification update except SoftTimeLimitExceeded: # Celery soft time limit was reached - task is about to be killed # Gracefully save progress and mark as interrupted @@ -2776,7 +2776,7 @@ async def run_composio_indexing( Run Composio connector indexing with real-time notifications. This wraps the Composio indexer with the notification system so that - Electric SQL can sync indexing progress to the frontend in real-time. + Zero can sync indexing progress to the frontend in real-time. Args: session: Database session diff --git a/surfsense_backend/app/services/chat_comments_service.py b/surfsense_backend/app/services/chat_comments_service.py index c2bb65aee..54662fe5b 100644 --- a/surfsense_backend/app/services/chat_comments_service.py +++ b/surfsense_backend/app/services/chat_comments_service.py @@ -456,7 +456,7 @@ async def create_comment( thread = message.thread comment = ChatComment( message_id=message_id, - thread_id=thread.id, # Denormalized for efficient Electric subscriptions + thread_id=thread.id, # Denormalized for efficient per-thread sync author_id=user.id, content=content, ) @@ -569,7 +569,7 @@ async def create_reply( thread = parent_comment.message.thread reply = ChatComment( message_id=parent_comment.message_id, - thread_id=thread.id, # Denormalized for efficient Electric subscriptions + thread_id=thread.id, # Denormalized for efficient per-thread sync parent_id=comment_id, author_id=user.id, content=content, diff --git a/surfsense_backend/app/services/notification_service.py b/surfsense_backend/app/services/notification_service.py index a759f3536..5e40a3b42 100644 --- a/surfsense_backend/app/services/notification_service.py +++ b/surfsense_backend/app/services/notification_service.py @@ -1,4 +1,4 @@ -"""Service for creating and managing notifications with Electric SQL sync.""" +"""Service for creating and managing notifications with Zero sync.""" import logging from datetime import UTC, datetime @@ -1045,7 +1045,7 @@ class PageLimitNotificationHandler(BaseNotificationHandler): class NotificationService: - """Service for creating and managing notifications that sync via Electric SQL.""" + """Service for creating and managing notifications that sync via Zero.""" # Handler instances connector_indexing = ConnectorIndexingNotificationHandler() @@ -1065,7 +1065,7 @@ class NotificationService: notification_metadata: dict[str, Any] | None = None, ) -> Notification: """ - Create a notification - Electric SQL will automatically sync it to frontend. + Create a notification - Zero will automatically sync it to frontend. Args: session: Database session diff --git a/surfsense_backend/app/tasks/celery_tasks/document_tasks.py b/surfsense_backend/app/tasks/celery_tasks/document_tasks.py index b0f08636a..a7da11749 100644 --- a/surfsense_backend/app/tasks/celery_tasks/document_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/document_tasks.py @@ -887,7 +887,7 @@ async def _process_file_with_document( ) try: - # Set status to PROCESSING (shows spinner in UI via ElectricSQL) + # Set status to PROCESSING (shows spinner in UI via Zero) document.status = DocumentStatus.processing() await session.commit() logger.info( @@ -951,7 +951,7 @@ async def _process_file_with_document( ): page_limit_error = e.__cause__ - # Mark document as failed (shows error in UI via ElectricSQL) + # Mark document as failed (shows error in UI via Zero) error_message = str(e)[:500] document.status = DocumentStatus.failed(error_message) document.updated_at = get_current_timestamp() diff --git a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py index 6f020685a..f77a0632a 100644 --- a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py @@ -139,7 +139,7 @@ async def index_airtable_records( await task_logger.log_task_success( log_entry, success_msg, {"bases_count": 0} ) - # CRITICAL: Update timestamp even when no bases found so Electric SQL syncs + # CRITICAL: Update timestamp even when no bases found so Zero syncs await update_connector_last_indexed( session, connector, update_last_indexed ) @@ -460,7 +460,7 @@ async def index_airtable_records( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) total_processed = documents_indexed diff --git a/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py index 0660531b2..8e64e56ba 100644 --- a/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py @@ -462,7 +462,7 @@ async def index_bookstack_pages( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs # This ensures the UI shows "Last indexed" instead of "Never indexed" await update_connector_last_indexed(session, connector, update_last_indexed) diff --git a/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py b/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py index af796ba3c..5a6cc3485 100644 --- a/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py @@ -470,7 +470,7 @@ async def index_clickup_tasks( total_processed = documents_indexed - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs # This ensures the UI shows "Last indexed" instead of "Never indexed" await update_connector_last_indexed(session, connector, update_last_indexed) diff --git a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py index 3495c59a4..3b46b6437 100644 --- a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py @@ -442,7 +442,7 @@ async def index_confluence_pages( documents_failed += 1 continue # Skip this page and continue with others - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs # This ensures the UI shows "Last indexed" instead of "Never indexed" await update_connector_last_indexed(session, connector, update_last_indexed) diff --git a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py index e8e80a646..5e784cb4f 100644 --- a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py @@ -718,7 +718,7 @@ async def index_discord_messages( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py b/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py index f07c6c580..3283b41eb 100644 --- a/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py @@ -413,7 +413,7 @@ async def index_elasticsearch_documents( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs # This ensures the UI shows "Last indexed" instead of "Never indexed" if update_last_indexed: connector.last_indexed_at = ( diff --git a/surfsense_backend/app/tasks/connector_indexers/github_indexer.py b/surfsense_backend/app/tasks/connector_indexers/github_indexer.py index 61607dda3..ae24d750b 100644 --- a/surfsense_backend/app/tasks/connector_indexers/github_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/github_indexer.py @@ -451,7 +451,7 @@ async def index_github_repos( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit diff --git a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py index 99fc666fa..233bc66e4 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py @@ -599,7 +599,7 @@ async def index_google_calendar_events( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py index 30f806c19..384ad85e2 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py @@ -519,7 +519,7 @@ async def index_google_gmail_messages( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py index 1765a592e..25491a8f6 100644 --- a/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/jira_indexer.py @@ -422,7 +422,7 @@ async def index_jira_issues( documents_failed += 1 continue # Skip this issue and continue with others - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs # This ensures the UI shows "Last indexed" instead of "Never indexed" await update_connector_last_indexed(session, connector, update_last_indexed) diff --git a/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py b/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py index bacafccc7..6e9ccaa01 100644 --- a/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py @@ -463,7 +463,7 @@ async def index_linear_issues( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py b/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py index 83cf54f4e..a698bfd46 100644 --- a/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py @@ -520,7 +520,7 @@ async def index_luma_events( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs # This ensures the UI shows "Last indexed" instead of "Never indexed" await update_connector_last_indexed(session, connector, update_last_indexed) diff --git a/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py b/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py index 85daff94c..619b8dcd7 100644 --- a/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py @@ -252,7 +252,7 @@ async def index_notion_pages( {"pages_found": 0}, ) logger.info("No Notion pages found to index") - # CRITICAL: Update timestamp even when no pages found so Electric SQL syncs + # CRITICAL: Update timestamp even when no pages found so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) await session.commit() await notion_client.close() @@ -506,7 +506,7 @@ async def index_notion_pages( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) total_processed = documents_indexed diff --git a/surfsense_backend/app/tasks/connector_indexers/obsidian_indexer.py b/surfsense_backend/app/tasks/connector_indexers/obsidian_indexer.py index d53baa3b0..5356ecfb7 100644 --- a/surfsense_backend/app/tasks/connector_indexers/obsidian_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/obsidian_indexer.py @@ -599,7 +599,7 @@ async def index_obsidian_vault( failed_count += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py index 1f2693844..2c6d0e11e 100644 --- a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py @@ -256,7 +256,7 @@ async def index_slack_messages( f"No Slack channels found for connector {connector_id}", {"channels_found": 0}, ) - # CRITICAL: Update timestamp even when no channels found so Electric SQL syncs + # CRITICAL: Update timestamp even when no channels found so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) await session.commit() return 0, None # Return None (not error) when no channels found @@ -593,7 +593,7 @@ async def index_slack_messages( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py b/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py index d04a98177..12cdf384e 100644 --- a/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py @@ -249,7 +249,7 @@ async def index_teams_messages( f"No Teams found for connector {connector_id}", {"teams_found": 0}, ) - # CRITICAL: Update timestamp even when no teams found so Electric SQL syncs + # CRITICAL: Update timestamp even when no teams found so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) await session.commit() return 0, None # Return None (not error) when no items found @@ -635,7 +635,7 @@ async def index_teams_messages( documents_failed += 1 continue - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py b/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py index 4d2644420..ada54e7fc 100644 --- a/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py @@ -444,7 +444,7 @@ async def index_crawled_urls( total_processed = documents_indexed + documents_updated - # CRITICAL: Always update timestamp (even if 0 documents indexed) so Electric SQL syncs + # CRITICAL: Always update timestamp (even if 0 documents indexed) so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit for any remaining documents not yet committed in batches diff --git a/surfsense_backend/tests/fixtures/sample.md b/surfsense_backend/tests/fixtures/sample.md index 7217540d8..9ba0cacdd 100644 --- a/surfsense_backend/tests/fixtures/sample.md +++ b/surfsense_backend/tests/fixtures/sample.md @@ -10,7 +10,7 @@ document upload pipeline. It includes various markdown formatting elements. - Document upload and processing - Automatic chunking of content - Embedding generation for semantic search -- Real-time status tracking via ElectricSQL +- Real-time status tracking via Zero ## Technical Architecture diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example index 9cb01786e..b674d8e9b 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -1,10 +1,7 @@ NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000 NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING - -# Electric SQL -NEXT_PUBLIC_ELECTRIC_URL=http://localhost:5133 -NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure +NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848 # Contact Form Vars (optional) DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres diff --git a/surfsense_web/Dockerfile b/surfsense_web/Dockerfile index 311c3c784..da6bc8b7e 100644 --- a/surfsense_web/Dockerfile +++ b/surfsense_web/Dockerfile @@ -35,15 +35,13 @@ RUN corepack enable pnpm ARG NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__ ARG NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__ ARG NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__ -ARG NEXT_PUBLIC_ELECTRIC_URL=__NEXT_PUBLIC_ELECTRIC_URL__ -ARG NEXT_PUBLIC_ELECTRIC_AUTH_MODE=__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__ +ARG NEXT_PUBLIC_ZERO_CACHE_URL=__NEXT_PUBLIC_ZERO_CACHE_URL__ ARG NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__ ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=$NEXT_PUBLIC_FASTAPI_BACKEND_URL ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=$NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE ENV NEXT_PUBLIC_ETL_SERVICE=$NEXT_PUBLIC_ETL_SERVICE -ENV NEXT_PUBLIC_ELECTRIC_URL=$NEXT_PUBLIC_ELECTRIC_URL -ENV NEXT_PUBLIC_ELECTRIC_AUTH_MODE=$NEXT_PUBLIC_ELECTRIC_AUTH_MODE +ENV NEXT_PUBLIC_ZERO_CACHE_URL=$NEXT_PUBLIC_ZERO_CACHE_URL ENV NEXT_PUBLIC_DEPLOYMENT_MODE=$NEXT_PUBLIC_DEPLOYMENT_MODE COPY --from=deps /app/node_modules ./node_modules diff --git a/surfsense_web/app/api/zero/mutate/route.ts b/surfsense_web/app/api/zero/mutate/route.ts new file mode 100644 index 000000000..0076e1ae8 --- /dev/null +++ b/surfsense_web/app/api/zero/mutate/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server"; + +export async function POST() { + return NextResponse.json([]); +} diff --git a/surfsense_web/app/api/zero/query/route.ts b/surfsense_web/app/api/zero/query/route.ts new file mode 100644 index 000000000..3d8ff0d33 --- /dev/null +++ b/surfsense_web/app/api/zero/query/route.ts @@ -0,0 +1,50 @@ +import { mustGetQuery } from "@rocicorp/zero"; +import { handleQueryRequest } from "@rocicorp/zero/server"; +import { NextResponse } from "next/server"; +import type { Context } from "@/types/zero"; +import { queries } from "@/zero/queries"; +import { schema } from "@/zero/schema"; + +const backendURL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000"; + +async function authenticateRequest( + request: Request +): Promise<{ ctx: Context; error?: never } | { ctx?: never; error: NextResponse }> { + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return { ctx: undefined }; + } + + try { + const res = await fetch(`${backendURL}/users/me`, { + headers: { Authorization: authHeader }, + }); + + if (!res.ok) { + return { error: NextResponse.json({ error: "Unauthorized" }, { status: 401 }) }; + } + + const user = await res.json(); + return { ctx: { userId: String(user.id) } }; + } catch { + return { error: NextResponse.json({ error: "Auth service unavailable" }, { status: 503 }) }; + } +} + +export async function POST(request: Request) { + const auth = await authenticateRequest(request); + if (auth.error) { + return auth.error; + } + + const result = await handleQueryRequest( + (name, args) => { + const query = mustGetQuery(queries, name); + return query.fn({ args, ctx: auth.ctx }); + }, + schema, + request + ); + + return NextResponse.json(result); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 23c640ceb..e7da4393f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -46,7 +46,6 @@ import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { GenerateReportToolUI } from "@/components/tool-ui/generate-report"; -import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation"; import { CreateGmailDraftToolUI, SendGmailEmailToolUI, @@ -81,9 +80,10 @@ import { import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute"; import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage"; import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory"; +import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation"; import { Skeleton } from "@/components/ui/skeleton"; import { useChatSessionStateSync } from "@/hooks/use-chat-session-state"; -import { useMessagesElectric } from "@/hooks/use-messages-electric"; +import { useMessagesSync } from "@/hooks/use-messages-sync"; import { documentsApiService } from "@/lib/apis/documents-api.service"; // import { WriteTodosToolUI } from "@/components/tool-ui/write-todos"; import { getBearerToken } from "@/lib/auth-utils"; @@ -258,13 +258,13 @@ export default function NewChatPage() { // Get current user for author info in shared chats const { data: currentUser } = useAtomValue(currentUserAtom); - // Live collaboration: sync session state and messages via Electric SQL + // Live collaboration: sync session state and messages via Zero useChatSessionStateSync(threadId); const { data: membersData } = useAtomValue(membersAtom); - const handleElectricMessagesUpdate = useCallback( + const handleSyncedMessagesUpdate = useCallback( ( - electricMessages: { + syncedMessages: { id: number; thread_id: number; role: string; @@ -278,11 +278,11 @@ export default function NewChatPage() { } setMessages((prev) => { - if (electricMessages.length < prev.length) { + if (syncedMessages.length < prev.length) { return prev; } - return electricMessages.map((msg) => { + return syncedMessages.map((msg) => { const member = msg.author_id ? membersData?.find((m) => m.user_id === msg.author_id) : null; @@ -309,7 +309,7 @@ export default function NewChatPage() { [isRunning, membersData] ); - useMessagesElectric(threadId, handleElectricMessagesUpdate); + useMessagesSync(threadId, handleSyncedMessagesUpdate); // Extract search_space_id from URL params const searchSpaceId = useMemo(() => { diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index b0aa58a8f..784fd3bcf 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -3,10 +3,10 @@ import "./globals.css"; import { RootProvider } from "fumadocs-ui/provider/next"; import { Roboto } from "next/font/google"; import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider"; -import { ElectricProvider } from "@/components/providers/ElectricProvider"; import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider"; import { I18nProvider } from "@/components/providers/I18nProvider"; import { PostHogProvider } from "@/components/providers/PostHogProvider"; +import { ZeroProvider } from "@/components/providers/ZeroProvider"; import { ThemeProvider } from "@/components/theme/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { LocaleProvider } from "@/contexts/LocaleContext"; @@ -141,9 +141,9 @@ export default function RootLayout({ > - + {children} - + diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts index 5a11ef3fc..f1f0bad72 100644 --- a/surfsense_web/app/sitemap.ts +++ b/surfsense_web/app/sitemap.ts @@ -213,7 +213,7 @@ export default function sitemap(): MetadataRoute.Sitemap { }, // How-to documentation { - url: "https://www.surfsense.com/docs/how-to/electric-sql", + url: "https://www.surfsense.com/docs/how-to/zero-sync", lastModified, changeFrequency: "daily", priority: 0.8, diff --git a/surfsense_web/components/UserDropdown.tsx b/surfsense_web/components/UserDropdown.tsx index 5b398ae0b..b79ab6e79 100644 --- a/surfsense_web/components/UserDropdown.tsx +++ b/surfsense_web/components/UserDropdown.tsx @@ -16,7 +16,6 @@ import { } from "@/components/ui/dropdown-menu"; import { Spinner } from "@/components/ui/spinner"; import { logout } from "@/lib/auth-utils"; -import { cleanupElectric } from "@/lib/electric/client"; import { resetUser, trackLogout } from "@/lib/posthog/events"; export function UserDropdown({ @@ -39,14 +38,6 @@ export function UserDropdown({ trackLogout(); resetUser(); - // Best-effort cleanup of Electric SQL / PGlite - // Even if this fails, login-time cleanup will handle it - try { - await cleanupElectric(); - } catch (err) { - console.warn("[Logout] Electric cleanup failed (will be handled on next login):", err); - } - // Revoke refresh token on server and clear all tokens from localStorage await logout(); diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 569b2ece4..3187d3c33 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -20,7 +20,7 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import { useConnectorsElectric } from "@/hooks/use-connectors-electric"; +import { useConnectorsSync } from "@/hooks/use-connectors-sync"; import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker"; import { cn } from "@/lib/utils"; import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; @@ -155,33 +155,23 @@ export const ConnectorIndicator = forwardRef 0 || (connectorsLoading && !connectorsError); - const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; + const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError); + const connectors = useSyncData ? connectorsFromSync : allConnectors || []; - // Manual refresh function that works with both Electric and API const refreshConnectors = async () => { - if (useElectricData) { - await refreshConnectorsElectric(); - } else { - // Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom) - // The connectorsAtom will handle refetching if needed + if (useSyncData) { + await refreshConnectorsSync(); } }; - // Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed + // Track indexing state locally - clears automatically when last_indexed_at changes via real-time sync // Also clears when failed notifications are detected const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( connectors as SearchSourceConnector[], @@ -202,7 +192,7 @@ export const ConnectorIndicator = forwardRef( (connectors || []).map((c: SearchSourceConnector) => c.connector_type) ); @@ -291,7 +281,7 @@ export const ConnectorIndicator = forwardRef c.id === editingConnector.id) ?.last_indexed_at ?? editingConnector.last_indexed_at, diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index c710347e3..7b4aa29fb 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -1254,7 +1254,7 @@ export const useConnectorDialog = () => { queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); // Note: Don't call stopIndexing here - let useIndexingConnectors hook - // detect when last_indexed_at changes via Electric SQL + // detect when last_indexed_at changes via real-time sync } catch (error) { console.error("Error indexing connector content:", error); toast.error(error instanceof Error ? error.message : "Failed to start indexing"); diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts index 5783540d8..fb778fc3b 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-indexing-connectors.ts @@ -48,13 +48,13 @@ function isTaskTimedOut(startedAt: string | null | undefined): boolean { * * This provides a better UX than polling by: * 1. Setting indexing state immediately when user triggers indexing (optimistic) - * 2. Detecting in_progress notifications from Electric SQL to restore state after remounts + * 2. Detecting in_progress notifications to restore state after remounts * 3. Clearing indexing state when notifications become completed or failed - * 4. Clearing indexing state when Electric SQL detects last_indexed_at changed + * 4. Clearing indexing state when real-time sync detects last_indexed_at changed * 5. Detecting stale/stuck tasks that haven't updated in 15+ minutes * 6. Detecting hard timeout (8h) - tasks that definitely cannot still be running * - * The actual `last_indexed_at` value comes from Electric SQL/PGlite, not local state. + * The actual `last_indexed_at` value comes from real-time sync, not local state. */ export function useIndexingConnectors( connectors: SearchSourceConnector[], @@ -66,7 +66,7 @@ export function useIndexingConnectors( // Track previous last_indexed_at values to detect changes const previousLastIndexedAtRef = useRef>(new Map()); - // Detect when last_indexed_at changes (indexing completed) via Electric SQL + // Detect when last_indexed_at changes (indexing completed) via real-time sync useEffect(() => { const previousValues = previousLastIndexedAtRef.current; diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index fa5595de4..081e234a8 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -97,7 +97,7 @@ import { } from "@/contracts/enums/toolIcons"; import type { Document } from "@/contracts/types/document.types"; import { useBatchCommentsPreload } from "@/hooks/use-comments"; -import { useCommentsElectric } from "@/hooks/use-comments-electric"; +import { useCommentsSync } from "@/hooks/use-comments-sync"; import { useMediaQuery } from "@/hooks/use-media-query"; import { cn } from "@/lib/utils"; @@ -371,8 +371,8 @@ const Composer: FC = () => { const respondingToUserId = sessionState?.respondingToUserId ?? null; const isBlockedByOtherUser = isAiResponding && respondingToUserId !== currentUser?.id; - // Sync comments for the entire thread via Electric SQL (one subscription per thread) - useCommentsElectric(threadId); + // Sync comments for the entire thread via Zero (one subscription per thread) + useCommentsSync(threadId); // Batch-prefetch comments for all assistant messages so individual useComments // hooks never fire their own network requests (eliminates N+1 API calls). @@ -1084,7 +1084,13 @@ const TOOL_GROUPS: ToolGroup[] = [ }, { label: "Generate", - tools: ["generate_podcast", "generate_video_presentation", "generate_report", "generate_image", "display_image"], + tools: [ + "generate_podcast", + "generate_video_presentation", + "generate_report", + "generate_image", + "display_image", + ], }, { label: "Memory", diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 9f302a91b..be139c586 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -54,7 +54,6 @@ import { notificationsApiService } from "@/lib/apis/notifications-api.service"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { logout } from "@/lib/auth-utils"; import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence"; -import { cleanupElectric } from "@/lib/electric/client"; import { resetUser, trackLogout } from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types"; @@ -155,8 +154,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid // Search space dialog state const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false); - // Per-tab inbox hooks — each has independent API loading, pagination, - // and Electric live queries. The Electric sync shape is shared (client-level cache). const userId = user?.id ? String(user.id) : null; const numericSpaceId = Number(searchSpaceId) || null; @@ -579,14 +576,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid trackLogout(); resetUser(); - // Best-effort cleanup of Electric SQL / PGlite - // Even if this fails, login-time cleanup will handle it - try { - await cleanupElectric(); - } catch (err) { - console.warn("[Logout] Electric cleanup failed (will be handled on next login):", err); - } - // Revoke refresh token on server and clear all tokens from localStorage await logout(); diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx deleted file mode 100644 index 02005d098..000000000 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { useAtomValue } from "jotai"; -import { usePathname } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; -import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; -import { getBearerToken } from "@/lib/auth-utils"; -import { - cleanupElectric, - type ElectricClient, - initElectric, - isElectricInitialized, -} from "@/lib/electric/client"; -import { ElectricContext } from "@/lib/electric/context"; - -const IS_DEV = process.env.NODE_ENV === "development"; - -interface ElectricProviderProps { - children: React.ReactNode; -} - -/** - * Initializes user-specific PGlite database with Electric SQL sync. - * Handles user isolation, cleanup, and re-initialization on user change. - */ -export function ElectricProvider({ children }: ElectricProviderProps) { - const [electricClient, setElectricClient] = useState(null); - const [error, setError] = useState(null); - const { - data: user, - isSuccess: isUserLoaded, - isError: isUserError, - } = useAtomValue(currentUserAtom); - const previousUserIdRef = useRef(null); - const initializingRef = useRef(false); - const pathname = usePathname(); - - useEffect(() => { - if (typeof window === "undefined") return; - - // No user logged in - cleanup if previous user existed - if (!isUserLoaded || !user?.id) { - if (previousUserIdRef.current && isElectricInitialized()) { - if (IS_DEV) console.log("[ElectricProvider] User logged out, cleaning up..."); - cleanupElectric().then(() => { - previousUserIdRef.current = null; - setElectricClient(null); - }); - } - return; - } - - const userId = String(user.id); - - // Skip if already initialized for this user or currently initializing - if ((electricClient && previousUserIdRef.current === userId) || initializingRef.current) { - return; - } - - initializingRef.current = true; - let mounted = true; - - async function init() { - try { - if (IS_DEV) console.log(`[ElectricProvider] Initializing for user: ${userId}`); - const client = await initElectric(userId); - - if (mounted) { - previousUserIdRef.current = userId; - setElectricClient(client); - setError(null); - if (IS_DEV) console.log(`[ElectricProvider] ✅ Ready for user: ${userId}`); - } - } catch (err) { - console.error("[ElectricProvider] Failed to initialize:", err); - if (mounted) { - setError(err instanceof Error ? err : new Error("Failed to initialize Electric SQL")); - setElectricClient(null); - } - } finally { - if (mounted) { - initializingRef.current = false; - } - } - } - - init(); - return () => { - mounted = false; - }; - }, [user?.id, isUserLoaded, electricClient]); - - const hasToken = typeof window !== "undefined" && !!getBearerToken(); - - // Only block UI on dashboard routes; public pages render immediately - const requiresElectricLoading = pathname?.startsWith("/dashboard"); - const shouldShowLoading = - hasToken && isUserLoaded && !!user?.id && !electricClient && !error && requiresElectricLoading; - - useGlobalLoadingEffect(shouldShowLoading); - - // Render immediately for unauthenticated users or failed user queries - if (!hasToken || !isUserLoaded || !user?.id || isUserError) { - return {children}; - } - - // Render with null context while initializing - if (!electricClient && !error) { - return {children}; - } - - if (error) { - console.warn("[ElectricProvider] Initialization failed, sync may not work:", error.message); - } - - return {children}; -} diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx new file mode 100644 index 000000000..f4df921f3 --- /dev/null +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { + useConnectionState, + useZero, + ZeroProvider as ZeroReactProvider, +} from "@rocicorp/zero/react"; +import { useAtomValue } from "jotai"; +import { useEffect, useRef } from "react"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { getBearerToken, handleUnauthorized, refreshAccessToken } from "@/lib/auth-utils"; +import { queries } from "@/zero/queries"; +import { schema } from "@/zero/schema"; + +const cacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848"; + +function ZeroAuthGuard({ children }: { children: React.ReactNode }) { + const zero = useZero(); + const connectionState = useConnectionState(); + const isRefreshingRef = useRef(false); + + useEffect(() => { + if (connectionState.name !== "needs-auth" || isRefreshingRef.current) return; + + isRefreshingRef.current = true; + + refreshAccessToken() + .then((newToken) => { + if (newToken) { + zero.connection.connect({ auth: newToken }); + } else { + handleUnauthorized(); + } + }) + .finally(() => { + isRefreshingRef.current = false; + }); + }, [connectionState, zero]); + + return <>{children}; +} + +export function ZeroProvider({ children }: { children: React.ReactNode }) { + const { data: user } = useAtomValue(currentUserAtom); + + const hasUser = !!user?.id; + const userID = hasUser ? String(user.id) : "anon"; + const context = hasUser ? { userId: String(user.id) } : undefined; + const auth = hasUser ? getBearerToken() || undefined : undefined; + + const opts = { + userID, + schema, + queries, + context, + cacheURL, + auth, + }; + + return ( + + {hasUser ? {children} : children} + + ); +} diff --git a/surfsense_web/components/public-chat/public-chat-view.tsx b/surfsense_web/components/public-chat/public-chat-view.tsx index cafc0d1a3..706d47ca6 100644 --- a/surfsense_web/components/public-chat/public-chat-view.tsx +++ b/surfsense_web/components/public-chat/public-chat-view.tsx @@ -6,9 +6,9 @@ import { ReportPanel } from "@/components/report-panel/report-panel"; import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { GenerateReportToolUI } from "@/components/tool-ui/generate-report"; -import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation"; import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview"; import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage"; +import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation"; import { Spinner } from "@/components/ui/spinner"; import { usePublicChat } from "@/hooks/use-public-chat"; import { usePublicChatRuntime } from "@/hooks/use-public-chat-runtime"; diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts index 65c0ca497..def531417 100644 --- a/surfsense_web/components/tool-ui/index.ts +++ b/surfsense_web/components/tool-ui/index.ts @@ -32,7 +32,6 @@ export { } from "./display-image"; export { GeneratePodcastToolUI } from "./generate-podcast"; export { GenerateReportToolUI } from "./generate-report"; -export { GenerateVideoPresentationToolUI } from "./video-presentation"; export { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "./google-drive"; export { Image, @@ -106,4 +105,5 @@ export { SaveMemoryResultSchema, SaveMemoryToolUI, } from "./user-memory"; +export { GenerateVideoPresentationToolUI } from "./video-presentation"; export { type WriteTodosData, WriteTodosSchema, WriteTodosToolUI } from "./write-todos"; diff --git a/surfsense_web/components/tool-ui/video-presentation/combined-player.tsx b/surfsense_web/components/tool-ui/video-presentation/combined-player.tsx index f8e79f677..9a87c48d2 100644 --- a/surfsense_web/components/tool-ui/video-presentation/combined-player.tsx +++ b/surfsense_web/components/tool-ui/video-presentation/combined-player.tsx @@ -1,9 +1,10 @@ "use client"; -import React, { useMemo } from "react"; -import { Player } from "@remotion/player"; -import { Sequence, AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion"; import { Audio } from "@remotion/media"; +import { Player } from "@remotion/player"; +import type React from "react"; +import { useMemo } from "react"; +import { AbsoluteFill, interpolate, Sequence, useCurrentFrame, useVideoConfig } from "remotion"; import { FPS } from "@/lib/remotion/constants"; export interface CompiledSlide { @@ -64,9 +65,7 @@ function Watermark() { ); } -export function buildSlideWithWatermark( - SlideComponent: React.ComponentType, -): React.FC { +export function buildSlideWithWatermark(SlideComponent: React.ComponentType): React.FC { const Wrapped: React.FC = () => ( @@ -115,7 +114,7 @@ export function CombinedPlayer({ slides }: CombinedPlayerProps) { const totalFrames = useMemo( () => slides.reduce((sum, s) => sum + s.durationInFrames, 0), - [slides], + [slides] ); return ( diff --git a/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx b/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx index 34e443f5d..0ec87264a 100644 --- a/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx +++ b/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx @@ -1,16 +1,9 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { makeAssistantToolUI } from "@assistant-ui/react"; -import { - AlertCircleIcon, - Download, - Film, - Loader2, - Presentation, - X, -} from "lucide-react"; +import { AlertCircleIcon, Download, Film, Loader2, Presentation, X } from "lucide-react"; import { useParams, usePathname } from "next/navigation"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { z } from "zod"; import { Spinner } from "@/components/ui/spinner"; import { baseApiService } from "@/lib/apis/base-api.service"; @@ -18,9 +11,9 @@ import { authenticatedFetch } from "@/lib/auth-utils"; import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check"; import { FPS } from "@/lib/remotion/constants"; import { - CombinedPlayer, buildCompositionComponent, buildSlideWithWatermark, + CombinedPlayer, type CompiledSlide, } from "./combined-player"; @@ -54,7 +47,7 @@ const VideoPresentationStatusResponseSchema = z.object({ audio_url: z.string().nullish(), duration_seconds: z.number().nullish(), duration_in_frames: z.number().nullish(), - }), + }) ) .nullish(), scene_codes: z @@ -63,7 +56,7 @@ const VideoPresentationStatusResponseSchema = z.object({ slide_number: z.number(), code: z.string(), title: z.string().nullish(), - }), + }) ) .nullish(), slide_count: z.number().nullish(), @@ -206,9 +199,7 @@ function VideoPresentationPlayer({ const durationInFrames = slide.duration_in_frames ?? 300; const check = compileCheck(scene.code); if (!check.success) { - console.warn( - `Slide ${slide.slide_number} failed to compile: ${check.error}`, - ); + console.warn(`Slide ${slide.slide_number} failed to compile: ${check.error}`); continue; } @@ -219,9 +210,7 @@ function VideoPresentationPlayer({ title: scene.title ?? slide.title, code: scene.code, durationInFrames, - audioUrl: slide.audio_url - ? `${backendUrl}${slide.audio_url}` - : undefined, + audioUrl: slide.audio_url ? `${backendUrl}${slide.audio_url}` : undefined, }); } @@ -238,17 +227,13 @@ function VideoPresentationPlayer({ try { let blob: Blob; if (shareToken) { - blob = await baseApiService.getBlob( - new URL(slide.audioUrl).pathname, - ); + blob = await baseApiService.getBlob(new URL(slide.audioUrl).pathname); } else { const resp = await authenticatedFetch(slide.audioUrl, { method: "GET", }); if (!resp.ok) { - console.warn( - `Audio fetch ${resp.status} for slide "${slide.title}"`, - ); + console.warn(`Audio fetch ${resp.status} for slide "${slide.title}"`); return { ...slide, audioUrl: undefined }; } blob = await resp.blob(); @@ -260,7 +245,7 @@ function VideoPresentationPlayer({ console.warn(`Failed to fetch audio for "${slide.title}":`, err); return { ...slide, audioUrl: undefined }; } - }), + }) ); setCompiledSlides(withBlobs); @@ -284,7 +269,7 @@ function VideoPresentationPlayer({ const totalDuration = useMemo( () => compiledSlides.reduce((sum, s) => sum + s.durationInFrames / FPS, 0), - [compiledSlides], + [compiledSlides] ); const handleDownload = async () => { @@ -299,9 +284,7 @@ function VideoPresentationPlayer({ abortControllerRef.current = controller; try { - const { canRenderMediaOnWeb, renderMediaOnWeb } = await import( - "@remotion/web-renderer" - ); + const { canRenderMediaOnWeb, renderMediaOnWeb } = await import("@remotion/web-renderer"); const formats = [ { container: "mp4" as const, videoCodec: "h264" as const, ext: "mp4" }, @@ -326,7 +309,7 @@ function VideoPresentationPlayer({ if (!chosen) { throw new Error( - "Your browser does not support video rendering (WebCodecs). Please use Chrome, Edge, or Firefox 130+.", + "Your browser does not support video rendering (WebCodecs). Please use Chrome, Edge, or Firefox 130+." ); } @@ -422,7 +405,7 @@ function VideoPresentationPlayer({ durationInFrames: slide.durationInFrames, fps: FPS, style: { width: 1920, height: 1080 }, - }), + }) ); }); @@ -466,8 +449,7 @@ function VideoPresentationPlayer({

{title}

- {compiledSlides.length} slides · {totalDuration.toFixed(1)}s ·{" "} - {FPS}fps + {compiledSlides.length} slides · {totalDuration.toFixed(1)}s · {FPS}fps

@@ -479,9 +461,7 @@ function VideoPresentationPlayer({ Rendering {renderFormat ?? ""}{" "} - {renderProgress !== null - ? `${Math.round(renderProgress * 100)}%` - : "..."} + {renderProgress !== null ? `${Math.round(renderProgress * 100)}%` : "..."}

Download Failed

-

- {renderError} -

+

{renderError}

)} @@ -626,8 +604,7 @@ export const GenerateVideoPresentationToolUI = makeAssistantToolUI< const params = useParams(); const pathname = usePathname(); const isPublicRoute = pathname?.startsWith("/public/"); - const shareToken = - isPublicRoute && typeof params?.token === "string" ? params.token : null; + const shareToken = isPublicRoute && typeof params?.token === "string" ? params.token : null; const title = args.video_title || "SurfSense Presentation"; diff --git a/surfsense_web/components/ui/hero-carousel.tsx b/surfsense_web/components/ui/hero-carousel.tsx index eb9584848..5760aad94 100644 --- a/surfsense_web/components/ui/hero-carousel.tsx +++ b/surfsense_web/components/ui/hero-carousel.tsx @@ -19,8 +19,7 @@ const carouselItems = [ }, { title: "Video Generation", - description: - "Create short videos with AI-generated visuals and narration from your sources.", + description: "Create short videos with AI-generated visuals and narration from your sources.", src: "/homepage/hero_tutorial/video_gen_surf.mp4", }, { diff --git a/surfsense_web/content/docs/docker-installation/dev-compose.mdx b/surfsense_web/content/docs/docker-installation/dev-compose.mdx index 302026c2a..599e9beb2 100644 --- a/surfsense_web/content/docs/docker-installation/dev-compose.mdx +++ b/surfsense_web/content/docs/docker-installation/dev-compose.mdx @@ -24,7 +24,7 @@ The following `.env` variables are **only used by the dev compose file** (they h | `REDIS_PORT` | Exposed Redis port (internal-only in prod) | `6379` | | `NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE` | Frontend build arg for auth type | `LOCAL` | | `NEXT_PUBLIC_ETL_SERVICE` | Frontend build arg for ETL service | `DOCLING` | +| `NEXT_PUBLIC_ZERO_CACHE_URL` | Frontend build arg for Zero-cache URL | `http://localhost:4848` | | `NEXT_PUBLIC_DEPLOYMENT_MODE` | Frontend build arg for deployment mode | `self-hosted` | -| `NEXT_PUBLIC_ELECTRIC_AUTH_MODE` | Frontend build arg for Electric auth | `insecure` | In the production compose file, the `NEXT_PUBLIC_*` frontend variables are automatically derived from `AUTH_TYPE`, `ETL_SERVICE`, and the port settings. In the dev compose file, they are passed as build args since the frontend is built from source. diff --git a/surfsense_web/content/docs/docker-installation/docker-compose.mdx b/surfsense_web/content/docs/docker-installation/docker-compose.mdx index c56f08106..1560d3759 100644 --- a/surfsense_web/content/docs/docker-installation/docker-compose.mdx +++ b/surfsense_web/content/docs/docker-installation/docker-compose.mdx @@ -18,8 +18,6 @@ After starting, access SurfSense at: - **Frontend**: [http://localhost:3929](http://localhost:3929) - **Backend API**: [http://localhost:8929](http://localhost:8929) - **API Docs**: [http://localhost:8929/docs](http://localhost:8929/docs) -- **Electric SQL**: [http://localhost:5929](http://localhost:5929) - --- ## Configuration @@ -50,7 +48,7 @@ All configuration lives in a single `docker/.env` file (or `surfsense/.env` if y |----------|-------------|---------| | `FRONTEND_PORT` | Frontend service port | `3929` | | `BACKEND_PORT` | Backend API service port | `8929` | -| `ELECTRIC_PORT` | Electric SQL service port | `5929` | +| `ZERO_CACHE_PORT` | Zero-cache real-time sync port | `5929` | ### Custom Domain / Reverse Proxy @@ -61,7 +59,18 @@ Only set these if serving SurfSense on a real domain via a reverse proxy (Caddy, | `NEXT_FRONTEND_URL` | Public frontend URL (e.g. `https://app.yourdomain.com`) | | `BACKEND_URL` | Public backend URL for OAuth callbacks (e.g. `https://api.yourdomain.com`) | | `NEXT_PUBLIC_FASTAPI_BACKEND_URL` | Backend URL used by the frontend (e.g. `https://api.yourdomain.com`) | -| `NEXT_PUBLIC_ELECTRIC_URL` | Electric SQL URL used by the frontend (e.g. `https://electric.yourdomain.com`) | +| `NEXT_PUBLIC_ZERO_CACHE_URL` | Zero-cache URL used by the frontend (e.g. `https://zero.yourdomain.com`) | + +### Zero-cache (Real-Time Sync) + +Defaults work out of the box. Change `ZERO_ADMIN_PASSWORD` for security in production. + +| Variable | Description | Default | +|----------|-------------|---------| +| `ZERO_ADMIN_PASSWORD` | Password for the zero-cache admin UI and `/statz` endpoint | `surfsense-zero-admin` | +| `ZERO_UPSTREAM_DB` | PostgreSQL connection URL for replication (must be a direct connection, not via pgbouncer) | *(built from DB_* vars)* | +| `ZERO_CVR_DB` | PostgreSQL connection URL for client view records | *(built from DB_* vars)* | +| `ZERO_CHANGE_DB` | PostgreSQL connection URL for replication log entries | *(built from DB_* vars)* | ### Database @@ -77,14 +86,6 @@ Defaults work out of the box. Change for security in production. | `DB_SSLMODE` | SSL mode: `disable`, `require`, `verify-ca`, `verify-full` | `disable` | | `DATABASE_URL` | Full connection URL override. Use for managed databases (RDS, Supabase, etc.) | *(built from above)* | -### Electric SQL - -| Variable | Description | Default | -|----------|-------------|---------| -| `ELECTRIC_DB_USER` | Replication user for Electric SQL | `electric` | -| `ELECTRIC_DB_PASSWORD` | Replication password for Electric SQL | `electric_password` | -| `ELECTRIC_DATABASE_URL` | Full connection URL override for Electric. Set to `host.docker.internal` when pointing at a local Postgres instance | *(built from above)* | - ### Authentication | Variable | Description | @@ -148,7 +149,7 @@ Uncomment the connectors you want to use. Redirect URIs follow the pattern `http | `backend` | FastAPI application server | | `celery_worker` | Background task processing (document indexing, etc.) | | `celery_beat` | Periodic task scheduler (connector sync) | -| `electric` | Electric SQL (real-time sync for the frontend) | +| `zero-cache` | Rocicorp Zero real-time sync (replicates Postgres to clients) | | `frontend` | Next.js web application | All services start automatically with `docker compose up -d`. @@ -165,7 +166,6 @@ docker compose logs -f # View logs for a specific service docker compose logs -f backend -docker compose logs -f electric # Stop all services docker compose down @@ -183,6 +183,6 @@ docker compose down -v - **Ports already in use**: Change the relevant `*_PORT` variable in `.env` and restart. - **Permission errors on Linux**: You may need to prefix `docker` commands with `sudo`. -- **Electric SQL not connecting**: Check `docker compose logs electric`. If it shows `domain does not exist: db`, ensure `ELECTRIC_DATABASE_URL` is not set to a stale value in `.env`. -- **Real-time updates not working in browser**: Open DevTools → Console and look for `[Electric]` errors. Check that `NEXT_PUBLIC_ELECTRIC_URL` matches the running Electric SQL address. +- **Zero-cache not starting**: Check `docker compose logs zero-cache`. Ensure PostgreSQL has `wal_level=logical` (configured automatically by the bundled `postgresql.conf`). +- **Real-time updates not working**: Open DevTools → Console and check for WebSocket errors. Verify `NEXT_PUBLIC_ZERO_CACHE_URL` matches the running zero-cache address. - **Line ending issues on Windows**: Run `git config --global core.autocrlf true` before cloning. diff --git a/surfsense_web/content/docs/docker-installation/install-script.mdx b/surfsense_web/content/docs/docker-installation/install-script.mdx index bbe95c230..50ccc7288 100644 --- a/surfsense_web/content/docs/docker-installation/install-script.mdx +++ b/surfsense_web/content/docs/docker-installation/install-script.mdx @@ -38,4 +38,4 @@ After starting, access SurfSense at: - **Frontend**: [http://localhost:3929](http://localhost:3929) - **Backend API**: [http://localhost:8929](http://localhost:8929) - **API Docs**: [http://localhost:8929/docs](http://localhost:8929/docs) -- **Electric SQL**: [http://localhost:5929](http://localhost:5929) +- **Zero-cache**: [http://localhost:5929](http://localhost:5929) diff --git a/surfsense_web/content/docs/how-to/electric-sql.mdx b/surfsense_web/content/docs/how-to/electric-sql.mdx deleted file mode 100644 index f051a9ab5..000000000 --- a/surfsense_web/content/docs/how-to/electric-sql.mdx +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: Electric SQL -description: Setting up Electric SQL for real-time data synchronization in SurfSense ---- - -[Electric SQL](https://electric-sql.com/) enables real-time data synchronization in SurfSense, providing instant updates for inbox items, document indexing status, and connector sync progress without manual refresh. The frontend uses [PGlite](https://pglite.dev/) (a lightweight PostgreSQL in the browser) to maintain a local database that syncs with the backend via Electric SQL. - -## What does Electric SQL do? - -When you index documents or receive inbox updates, Electric SQL pushes updates to your browser in real-time. The data flows like this: - -1. Backend writes data to PostgreSQL -2. Electric SQL detects changes and streams them to the frontend -3. PGlite (running in your browser) receives and stores the data locally in IndexedDB -4. Your UI updates instantly without refreshing - -This means: - -- **Inbox updates appear instantly** - No need to refresh the page -- **Document indexing progress updates live** - Watch your documents get processed -- **Connector status syncs automatically** - See when connectors finish syncing -- **Offline support** - PGlite caches data locally, so previously loaded data remains accessible - -## Docker Setup - -- The `docker-compose.yml` includes the Electric SQL service, pre-configured to connect to the Docker-managed `db` container. -- No additional configuration is required. Electric SQL works with the Docker PostgreSQL instance out of the box. - -## Manual Setup (Development Only) - -This section is intended for local development environments. Follow the steps below based on your PostgreSQL setup. - -### Step 1: Configure Environment Variables - -Ensure your environment files are configured. If you haven't set up SurfSense yet, follow the [Manual Installation Guide](/docs/manual-installation) first. - -For Electric SQL, verify these variables are set: - -**Backend (`surfsense_backend/.env`):** - -```bash -ELECTRIC_DB_USER=electric -ELECTRIC_DB_PASSWORD=electric_password -``` - -**Frontend (`surfsense_web/.env`):** - -```bash -NEXT_PUBLIC_ELECTRIC_URL=http://localhost:5133 -NEXT_PUBLIC_ELECTRIC_AUTH_MODE=insecure -``` - -Next, choose the option that matches your PostgreSQL setup: - ---- - -### Option A: Using Docker PostgreSQL - -If you're using the Docker-managed PostgreSQL instance, no extra configuration is needed. Just start the services using the development compose file (which exposes the PostgreSQL port to your host machine): - -```bash -docker compose -f docker-compose.dev.yml up -d db electric -``` - -Then run the database migration, start the backend, and launch the frontend: - -```bash -cd surfsense_backend -uv run alembic upgrade head -uv run main.py -``` - -In a separate terminal, start the frontend: - -```bash -cd surfsense_web -pnpm run dev -``` - -Electric SQL is now configured and connected to your Docker PostgreSQL database. - ---- - -### Option B: Using Local PostgreSQL - -If you're using a local PostgreSQL installation (e.g. Postgres.app on macOS), follow these steps: - -**1. Enable logical replication in PostgreSQL:** - -Open your `postgresql.conf` file: - -```bash -# Common locations: -# macOS (Postgres.app): ~/Library/Application Support/Postgres/var-17/postgresql.conf -# macOS (Homebrew): /opt/homebrew/var/postgresql@17/postgresql.conf -# Linux: /etc/postgresql/17/main/postgresql.conf - -sudo vim /path/to/postgresql.conf -``` - -Add the following settings: - -```ini -# Required for Electric SQL -wal_level = logical -max_replication_slots = 10 -max_wal_senders = 10 -``` - -After saving, restart PostgreSQL for the settings to take effect. - -**2. Create the Electric replication user:** - -Connect to your local database as a superuser and run: - -```sql -CREATE USER electric WITH REPLICATION PASSWORD 'electric_password'; -GRANT CONNECT ON DATABASE surfsense TO electric; -GRANT CREATE ON DATABASE surfsense TO electric; -GRANT USAGE ON SCHEMA public TO electric; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO electric; -GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO electric; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO electric; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO electric; -CREATE PUBLICATION electric_publication_default; -``` - -**3. Set `ELECTRIC_DATABASE_URL` in `docker/.env`:** - -Uncomment and update this line to point Electric at your local Postgres via `host.docker.internal` (the hostname Docker containers use to reach the host machine): - -```bash -ELECTRIC_DATABASE_URL=postgresql://electric:electric_password@host.docker.internal:5432/surfsense?sslmode=disable -``` - -**4. Start Electric SQL only (skip the Docker `db` container):** - -```bash -docker compose -f docker-compose.dev.yml up -d --no-deps electric -``` - -The `--no-deps` flag starts only the `electric` service without starting the Docker-managed `db` container. - -**5. Run database migration and start the backend:** - -```bash -cd surfsense_backend -uv run alembic upgrade head -uv run main.py -``` - -In a separate terminal, start the frontend: - -```bash -cd surfsense_web -pnpm run dev -``` - -Electric SQL is now configured and connected to your local PostgreSQL database. - -## Environment Variables Reference - -**Required for manual setup:** - -| Variable | Location | Description | Default | -|----------|----------|-------------|---------| -| `ELECTRIC_DB_USER` | `surfsense_backend/.env` | Database user for Electric replication | `electric` | -| `ELECTRIC_DB_PASSWORD` | `surfsense_backend/.env` | Database password for Electric replication | `electric_password` | -| `NEXT_PUBLIC_ELECTRIC_URL` | `surfsense_web/.env` | Electric SQL server URL (PGlite connects to this) | `http://localhost:5133` | -| `NEXT_PUBLIC_ELECTRIC_AUTH_MODE` | `surfsense_web/.env` | Authentication mode (`insecure` for dev, `secure` for production) | `insecure` | - -**Optional / Docker-only:** - -| Variable | Location | Description | Default | -|----------|----------|-------------|---------| -| `ELECTRIC_PORT` | `docker/.env` | Port to expose Electric SQL on the host | `5133` (dev), `5929` (production) | -| `ELECTRIC_DATABASE_URL` | `docker/.env` | Full connection URL override for Electric. Only needed for Option B (local Postgres via `host.docker.internal`) | *(built from above defaults)* | - -## Verify Setup - -To verify Electric SQL is running correctly: - -```bash -curl http://localhost:5133/v1/health -``` - -You should receive: - -```json -{"status":"active"} -``` - -## Troubleshooting - -### Electric SQL Server Not Starting - -**Check PostgreSQL settings:** -- Ensure `wal_level = logical` is set -- Verify the Electric user has replication permissions -- Check database connectivity from Electric container - -### Real-time Updates Not Working - -1. Open browser DevTools → Console -2. Look for errors containing `[Electric]` -3. Check Network tab for WebSocket connections to the Electric URL - -### Connection Refused Errors - -- Verify Electric SQL server is running: `docker ps | grep electric` -- Check the `NEXT_PUBLIC_ELECTRIC_URL` matches your Electric server address -- For Docker setups, ensure the frontend can reach the Electric container - -### Data Not Syncing - -- Check Electric SQL logs: `docker compose logs electric` -- Verify PostgreSQL replication is working -- Ensure the Electric user has proper table permissions - -### PGlite/IndexedDB Issues - -If data appears stale or corrupted in the browser: - -1. Open browser DevTools → Application → IndexedDB -2. Delete databases starting with `surfsense-` -3. Refresh the page - PGlite will recreate the local database and resync diff --git a/surfsense_web/content/docs/how-to/index.mdx b/surfsense_web/content/docs/how-to/index.mdx index 971df3512..be8b6f8cd 100644 --- a/surfsense_web/content/docs/how-to/index.mdx +++ b/surfsense_web/content/docs/how-to/index.mdx @@ -9,9 +9,9 @@ Practical guides to help you get the most out of SurfSense. ` | + +### Manual / Local Development + +If running the frontend outside Docker (e.g., `pnpm dev`), you need: + +1. A running zero-cache instance pointing at your PostgreSQL database +2. `NEXT_PUBLIC_ZERO_CACHE_URL` set in your `.env` file (default: `http://localhost:4848`) + +### Custom Domain / Reverse Proxy + +When deploying behind a reverse proxy, set `NEXT_PUBLIC_ZERO_CACHE_URL` to your public zero-cache URL (e.g., `https://zero.yourdomain.com`). The zero-cache service must be accessible via WebSocket from the browser. + +### Database Requirements + +zero-cache connects to PostgreSQL using logical replication. The database must meet these requirements: + +1. **`wal_level = logical`** — already configured in the bundled `postgresql.conf` +2. **The database user must have `REPLICATION` privilege** — required for creating logical replication slots + +In the default Docker setup, the `surfsense` user is a PostgreSQL superuser and has all required privileges automatically. + +**For managed databases** (RDS, Supabase, Cloud SQL, etc.) where the app user may not be a superuser, you need to grant replication privileges: + +```sql +ALTER USER surfsense WITH REPLICATION; +GRANT CREATE ON DATABASE surfsense TO surfsense; +``` + +The `REPLICATION` privilege allows zero-cache to create a logical replication slot for streaming changes. The `CREATE` privilege allows zero-cache to create internal schemas (`zero`, `zero_0`) for its metadata. + +## Synced Tables + +Zero syncs the following tables for real-time features: + +| Table | Used By | +|-------|---------| +| `notifications` | Inbox (comments, document processing, connector status) | +| `documents` | Document list, processing status indicators | +| `search_source_connectors` | Connector status, indexing progress | +| `new_chat_messages` | Live chat message sync for shared chats | +| `chat_comments` | Real-time comment threads on AI responses | +| `chat_session_state` | Collaboration indicators (who is typing) | + +## Troubleshooting + +- **zero-cache not starting**: Check `docker compose logs zero-cache`. Ensure PostgreSQL has `wal_level=logical` (configured in `postgresql.conf`). +- **Frontend not syncing**: Open DevTools → Console and check for WebSocket connection errors. Verify `NEXT_PUBLIC_ZERO_CACHE_URL` matches the running zero-cache address. +- **Stale data after restart**: zero-cache rebuilds its SQLite replica from PostgreSQL on startup. This may take a moment for large databases. + +## Learn More + +- [Rocicorp Zero Documentation](https://zero.rocicorp.dev/docs) +- [Zero Schema Reference](https://zero.rocicorp.dev/docs/schema) +- [Zero Deployment Guide](https://zero.rocicorp.dev/docs/deployment) diff --git a/surfsense_web/content/docs/manual-installation.mdx b/surfsense_web/content/docs/manual-installation.mdx index 96f272717..1577b8d8b 100644 --- a/surfsense_web/content/docs/manual-installation.mdx +++ b/surfsense_web/content/docs/manual-installation.mdx @@ -73,8 +73,6 @@ Edit the `.env` file and set the following variables: | AUTH_TYPE | Authentication method: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication | | GOOGLE_OAUTH_CLIENT_ID | (Optional) Client ID from Google Cloud Console (required if AUTH_TYPE=GOOGLE) | | GOOGLE_OAUTH_CLIENT_SECRET | (Optional) Client secret from Google Cloud Console (required if AUTH_TYPE=GOOGLE) | -| ELECTRIC_DB_USER | (Optional) PostgreSQL username for Electric-SQL connection (default: `electric`) | -| ELECTRIC_DB_PASSWORD | (Optional) PostgreSQL password for Electric-SQL connection (default: `electric_password`) | | EMBEDDING_MODEL | Name of the embedding model (e.g., `sentence-transformers/all-MiniLM-L6-v2`, `openai://text-embedding-ada-002`) | | RERANKERS_ENABLED | (Optional) Enable or disable document reranking for improved search results (e.g., `TRUE` or `FALSE`, default: `FALSE`) | | RERANKERS_MODEL_NAME | Name of the reranker model (e.g., `ms-marco-MiniLM-L-12-v2`) (required if RERANKERS_ENABLED=TRUE) | @@ -410,8 +408,7 @@ Edit the `.env` file and set: | NEXT_PUBLIC_FASTAPI_BACKEND_URL | Backend URL (e.g., `http://localhost:8000`) | | NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE | Same value as set in backend AUTH_TYPE i.e `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication | | NEXT_PUBLIC_ETL_SERVICE | Document parsing service (should match backend ETL_SERVICE): `UNSTRUCTURED`, `LLAMACLOUD`, or `DOCLING` - affects supported file formats in upload interface | -| NEXT_PUBLIC_ELECTRIC_URL | URL for Electric-SQL service (e.g., `http://localhost:5133`) | -| NEXT_PUBLIC_ELECTRIC_AUTH_MODE | Electric-SQL authentication mode (default: `insecure`) | +| NEXT_PUBLIC_ZERO_CACHE_URL | URL for Zero-cache real-time sync service (e.g., `http://localhost:4848`) | ### 2. Install Dependencies diff --git a/surfsense_web/contracts/types/chat-comments.types.ts b/surfsense_web/contracts/types/chat-comments.types.ts index cdeca0a44..a7751917e 100644 --- a/surfsense_web/contracts/types/chat-comments.types.ts +++ b/surfsense_web/contracts/types/chat-comments.types.ts @@ -6,7 +6,7 @@ import { z } from "zod"; export const rawComment = z.object({ id: z.number(), message_id: z.number(), - thread_id: z.number(), // Denormalized for efficient Electric subscriptions + thread_id: z.number(), // Denormalized for efficient per-thread sync parent_id: z.number().nullable(), author_id: z.string().nullable(), content: z.string(), diff --git a/surfsense_web/contracts/types/chat-messages.types.ts b/surfsense_web/contracts/types/chat-messages.types.ts index 78bf7b043..0859f9f3b 100644 --- a/surfsense_web/contracts/types/chat-messages.types.ts +++ b/surfsense_web/contracts/types/chat-messages.types.ts @@ -1,7 +1,7 @@ import { z } from "zod"; /** - * Raw message from database (Electric SQL sync) + * Raw message from database (real-time sync) */ export const rawMessage = z.object({ id: z.number(), diff --git a/surfsense_web/docker-entrypoint.js b/surfsense_web/docker-entrypoint.js index a4110657d..8323f5652 100644 --- a/surfsense_web/docker-entrypoint.js +++ b/surfsense_web/docker-entrypoint.js @@ -17,14 +17,16 @@ const replacements = [ "__NEXT_PUBLIC_FASTAPI_BACKEND_URL__", process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000", ], - ["__NEXT_PUBLIC_ELECTRIC_URL__", process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133"], [ "__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__", process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "LOCAL", ], ["__NEXT_PUBLIC_ETL_SERVICE__", process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING"], + [ + "__NEXT_PUBLIC_ZERO_CACHE_URL__", + process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848", + ], ["__NEXT_PUBLIC_DEPLOYMENT_MODE__", process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted"], - ["__NEXT_PUBLIC_ELECTRIC_AUTH_MODE__", process.env.NEXT_PUBLIC_ELECTRIC_AUTH_MODE || "insecure"], ]; let filesProcessed = 0; diff --git a/surfsense_web/hooks/use-chat-session-state.ts b/surfsense_web/hooks/use-chat-session-state.ts index f3bdd7722..467e360aa 100644 --- a/surfsense_web/hooks/use-chat-session-state.ts +++ b/surfsense_web/hooks/use-chat-session-state.ts @@ -1,27 +1,19 @@ "use client"; -import { useShape } from "@electric-sql/react"; +import { useQuery } from "@rocicorp/zero/react"; import { useSetAtom } from "jotai"; import { useEffect } from "react"; import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; -import type { ChatSessionState } from "@/contracts/types/chat-session-state.types"; - -const ELECTRIC_URL = process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133"; +import { queries } from "@/zero/queries"; /** - * Syncs chat session state for a thread via Electric SQL. + * Syncs chat session state for a thread via Zero. * Call once per thread (in page.tsx). Updates global atom. */ export function useChatSessionStateSync(threadId: number | null) { const setSessionState = useSetAtom(chatSessionStateAtom); - const { data } = useShape({ - url: `${ELECTRIC_URL}/v1/shape`, - params: { - table: "chat_session_state", - where: `thread_id = ${threadId ?? -1}`, - }, - }); + const [row] = useQuery(queries.chatSession.byThread({ threadId: threadId ?? -1 })); useEffect(() => { if (!threadId) { @@ -29,11 +21,10 @@ export function useChatSessionStateSync(threadId: number | null) { return; } - const row = data?.[0]; setSessionState({ threadId, - isAiResponding: !!row?.ai_responding_to_user_id, - respondingToUserId: row?.ai_responding_to_user_id ?? null, + isAiResponding: !!row?.aiRespondingToUserId, + respondingToUserId: row?.aiRespondingToUserId ?? null, }); - }, [threadId, data, setSessionState]); + }, [threadId, row, setSessionState]); } diff --git a/surfsense_web/hooks/use-comments-electric.ts b/surfsense_web/hooks/use-comments-electric.ts deleted file mode 100644 index 6ca7748b5..000000000 --- a/surfsense_web/hooks/use-comments-electric.ts +++ /dev/null @@ -1,413 +0,0 @@ -"use client"; - -import { useQueryClient } from "@tanstack/react-query"; -import { useAtomValue } from "jotai"; -import { useCallback, useEffect, useMemo, useRef } from "react"; -import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; -import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import type { Author, Comment, CommentReply } from "@/contracts/types/chat-comments.types"; -import type { Membership } from "@/contracts/types/members.types"; -import type { SyncHandle } from "@/lib/electric/client"; -import { useElectricClient } from "@/lib/electric/context"; -import { cacheKeys } from "@/lib/query-client/cache-keys"; - -// Debounce delay for stream updates (ms) -const STREAM_UPDATE_DEBOUNCE_MS = 100; - -// Raw comment from PGlite local database -interface RawCommentRow { - id: number; - message_id: number; - thread_id: number; - parent_id: number | null; - author_id: string | null; - content: string; - created_at: string; - updated_at: string; -} - -// Regex pattern to match @[uuid] mentions (matches backend MENTION_PATTERN) -const MENTION_PATTERN = /@\[([0-9a-fA-F-]{36})\]/g; - -type MemberInfo = Pick; - -/** - * Render mentions in content by replacing @[uuid] with @{DisplayName} - */ -function renderMentions(content: string, memberMap: Map): string { - return content.replace(MENTION_PATTERN, (match, uuid) => { - const member = memberMap.get(uuid); - if (member?.user_display_name) { - return `@{${member.user_display_name}}`; - } - return match; - }); -} - -/** - * Build member lookup map from membersData - */ -function buildMemberMap(membersData: Membership[] | undefined): Map { - const map = new Map(); - if (membersData) { - for (const m of membersData) { - map.set(m.user_id, { - user_display_name: m.user_display_name, - user_avatar_url: m.user_avatar_url, - user_email: m.user_email, - }); - } - } - return map; -} - -/** - * Build author object from member data - */ -function buildAuthor(authorId: string | null, memberMap: Map): Author | null { - if (!authorId) return null; - const m = memberMap.get(authorId); - if (!m) return null; - return { - id: authorId, - display_name: m.user_display_name ?? null, - avatar_url: m.user_avatar_url ?? null, - email: m.user_email ?? "", - }; -} - -/** - * Check if a comment has been edited by comparing timestamps. - * Uses a small threshold to handle precision differences. - */ -function isEdited(createdAt: string, updatedAt: string): boolean { - const created = new Date(createdAt).getTime(); - const updated = new Date(updatedAt).getTime(); - // Consider edited if updated_at is more than 1 second after created_at - return updated - created > 1000; -} - -/** - * Transform raw comment to CommentReply - */ -function transformReply( - raw: RawCommentRow, - memberMap: Map, - currentUserId: string | undefined, - isOwner: boolean -): CommentReply { - return { - id: raw.id, - content: raw.content, - content_rendered: renderMentions(raw.content, memberMap), - author: buildAuthor(raw.author_id, memberMap), - created_at: raw.created_at, - updated_at: raw.updated_at, - is_edited: isEdited(raw.created_at, raw.updated_at), - can_edit: currentUserId === raw.author_id, - can_delete: currentUserId === raw.author_id || isOwner, - }; -} - -/** - * Transform raw comments to Comment with replies - */ -function transformComments( - rawComments: RawCommentRow[], - memberMap: Map, - currentUserId: string | undefined, - isOwner: boolean -): Map { - // Group comments by message_id - const byMessage = new Map< - number, - { topLevel: RawCommentRow[]; replies: Map } - >(); - - for (const raw of rawComments) { - if (!byMessage.has(raw.message_id)) { - byMessage.set(raw.message_id, { topLevel: [], replies: new Map() }); - } - const group = byMessage.get(raw.message_id)!; - - if (raw.parent_id === null) { - group.topLevel.push(raw); - } else { - if (!group.replies.has(raw.parent_id)) { - group.replies.set(raw.parent_id, []); - } - group.replies.get(raw.parent_id)!.push(raw); - } - } - - // Transform to Comment objects grouped by message_id - const result = new Map(); - - for (const [messageId, group] of byMessage) { - const comments: Comment[] = group.topLevel.map((raw) => { - const replies = (group.replies.get(raw.id) || []) - .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) - .map((r) => transformReply(r, memberMap, currentUserId, isOwner)); - - return { - id: raw.id, - message_id: raw.message_id, - content: raw.content, - content_rendered: renderMentions(raw.content, memberMap), - author: buildAuthor(raw.author_id, memberMap), - created_at: raw.created_at, - updated_at: raw.updated_at, - is_edited: isEdited(raw.created_at, raw.updated_at), - can_edit: currentUserId === raw.author_id, - can_delete: currentUserId === raw.author_id || isOwner, - reply_count: replies.length, - replies, - }; - }); - - // Sort by created_at - comments.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); - result.set(messageId, comments); - } - - return result; -} - -/** - * Hook for syncing comments with Electric SQL real-time sync. - * - * Syncs ALL comments for a thread in ONE subscription, then updates - * React Query cache for each message. This avoids N subscriptions for N messages. - * - * @param threadId - The thread ID to sync comments for - */ -export function useCommentsElectric(threadId: number | null) { - const electricClient = useElectricClient(); - const queryClient = useQueryClient(); - - const { data: membersData } = useAtomValue(membersAtom); - const { data: currentUser } = useAtomValue(currentUserAtom); - const { data: myAccess } = useAtomValue(myAccessAtom); - - const memberMap = useMemo(() => buildMemberMap(membersData), [membersData]); - const currentUserId = currentUser?.id; - const isOwner = myAccess?.is_owner ?? false; - - // Use refs for values needed in live query callback to avoid stale closures - const memberMapRef = useRef(memberMap); - const currentUserIdRef = useRef(currentUserId); - const isOwnerRef = useRef(isOwner); - const queryClientRef = useRef(queryClient); - - // Keep refs updated - useEffect(() => { - memberMapRef.current = memberMap; - currentUserIdRef.current = currentUserId; - isOwnerRef.current = isOwner; - queryClientRef.current = queryClient; - }, [memberMap, currentUserId, isOwner, queryClient]); - - const syncHandleRef = useRef(null); - const liveQueryRef = useRef<{ unsubscribe: () => void } | null>(null); - const syncKeyRef = useRef(null); - const streamUpdateDebounceRef = useRef | null>(null); - - // Stable callback that uses refs for fresh values - const updateReactQueryCache = useCallback((rows: RawCommentRow[]) => { - const commentsByMessage = transformComments( - rows, - memberMapRef.current, - currentUserIdRef.current, - isOwnerRef.current - ); - - for (const [messageId, comments] of commentsByMessage) { - const cacheKey = cacheKeys.comments.byMessage(messageId); - queryClientRef.current.setQueryData(cacheKey, { - comments, - total_count: comments.length, - }); - } - }, []); - - useEffect(() => { - if (!threadId || !electricClient) { - return; - } - - const syncKey = `comments_${threadId}`; - if (syncKeyRef.current === syncKey) { - return; - } - - // Capture in local variable for use in async functions - const client = electricClient; - - let mounted = true; - syncKeyRef.current = syncKey; - - async function startSync() { - try { - const handle = await client.syncShape({ - table: "chat_comments", - where: `thread_id = ${threadId}`, - columns: [ - "id", - "message_id", - "thread_id", - "parent_id", - "author_id", - "content", - "created_at", - "updated_at", - ], - primaryKey: ["id"], - }); - - if (!handle.isUpToDate && handle.initialSyncPromise) { - try { - await Promise.race([ - handle.initialSyncPromise, - new Promise((resolve) => setTimeout(resolve, 3000)), - ]); - } catch { - // Initial sync timeout - continue anyway - } - } - - if (!mounted) { - handle.unsubscribe(); - return; - } - - syncHandleRef.current = handle; - - // Fetch initial comments and update cache - await fetchAndUpdateCache(); - - // Set up live query for real-time updates - await setupLiveQuery(); - - // Subscribe to the sync stream for real-time updates from Electric SQL - // This ensures we catch updates even if PGlite live query misses them - if (handle.stream) { - const stream = handle.stream as { - subscribe?: (callback: (messages: unknown[]) => void) => void; - }; - if (typeof stream.subscribe === "function") { - stream.subscribe((messages: unknown[]) => { - if (!mounted) return; - // When Electric sync receives new data, refresh from PGlite - // This handles cases where live query might miss the update - if (messages && messages.length > 0) { - // Debounce the refresh to avoid excessive queries - if (streamUpdateDebounceRef.current) { - clearTimeout(streamUpdateDebounceRef.current); - } - streamUpdateDebounceRef.current = setTimeout(() => { - if (mounted) { - fetchAndUpdateCache(); - } - }, STREAM_UPDATE_DEBOUNCE_MS); - } - }); - } - } - } catch { - // Sync failed - will retry on next mount - } - } - - async function fetchAndUpdateCache() { - try { - const result = await client.db.query( - `SELECT id, message_id, thread_id, parent_id, author_id, content, created_at, updated_at - FROM chat_comments - WHERE thread_id = $1 - ORDER BY created_at ASC`, - [threadId] - ); - - if (mounted && result.rows) { - updateReactQueryCache(result.rows); - } - } catch { - // Query failed - data will be fetched from API - } - } - - async function setupLiveQuery() { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const db = client.db as any; - - if (db.live?.query && typeof db.live.query === "function") { - const liveQuery = await db.live.query( - `SELECT id, message_id, thread_id, parent_id, author_id, content, created_at, updated_at - FROM chat_comments - WHERE thread_id = $1 - ORDER BY created_at ASC`, - [threadId] - ); - - if (!mounted) { - liveQuery.unsubscribe?.(); - return; - } - - // Set initial results - if (liveQuery.initialResults?.rows) { - updateReactQueryCache(liveQuery.initialResults.rows); - } else if (liveQuery.rows) { - updateReactQueryCache(liveQuery.rows); - } - - // Subscribe to changes - if (typeof liveQuery.subscribe === "function") { - liveQuery.subscribe((result: { rows: RawCommentRow[] }) => { - if (mounted && result.rows) { - updateReactQueryCache(result.rows); - } - }); - } - - if (typeof liveQuery.unsubscribe === "function") { - liveQueryRef.current = liveQuery; - } - } - } catch { - // Live query setup failed - will use initial fetch only - } - } - - startSync(); - - return () => { - mounted = false; - syncKeyRef.current = null; - - // Clear debounce timeout - if (streamUpdateDebounceRef.current) { - clearTimeout(streamUpdateDebounceRef.current); - streamUpdateDebounceRef.current = null; - } - - if (syncHandleRef.current) { - try { - syncHandleRef.current.unsubscribe(); - } catch { - // PGlite may already be closed during cleanup - } - syncHandleRef.current = null; - } - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe(); - } catch { - // PGlite may already be closed during cleanup - } - liveQueryRef.current = null; - } - }; - }, [threadId, electricClient, updateReactQueryCache]); -} diff --git a/surfsense_web/hooks/use-comments-sync.ts b/surfsense_web/hooks/use-comments-sync.ts new file mode 100644 index 000000000..b6a68364d --- /dev/null +++ b/surfsense_web/hooks/use-comments-sync.ts @@ -0,0 +1,212 @@ +"use client"; + +import { useQuery } from "@rocicorp/zero/react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import type { Author, Comment, CommentReply } from "@/contracts/types/chat-comments.types"; +import type { Membership } from "@/contracts/types/members.types"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { queries } from "@/zero/queries"; + +interface RawCommentRow { + id: number; + message_id: number; + thread_id: number; + parent_id: number | null; + author_id: string | null; + content: string; + created_at: string; + updated_at: string; +} + +const MENTION_PATTERN = /@\[([0-9a-fA-F-]{36})\]/g; + +type MemberInfo = Pick; + +function renderMentions(content: string, memberMap: Map): string { + return content.replace(MENTION_PATTERN, (match, uuid) => { + const member = memberMap.get(uuid); + if (member?.user_display_name) { + return `@{${member.user_display_name}}`; + } + return match; + }); +} + +function buildMemberMap(membersData: Membership[] | undefined): Map { + const map = new Map(); + if (membersData) { + for (const m of membersData) { + map.set(m.user_id, { + user_display_name: m.user_display_name, + user_avatar_url: m.user_avatar_url, + user_email: m.user_email, + }); + } + } + return map; +} + +function buildAuthor(authorId: string | null, memberMap: Map): Author | null { + if (!authorId) return null; + const m = memberMap.get(authorId); + if (!m) return null; + return { + id: authorId, + display_name: m.user_display_name ?? null, + avatar_url: m.user_avatar_url ?? null, + email: m.user_email ?? "", + }; +} + +function isEdited(createdAt: string, updatedAt: string): boolean { + const created = new Date(createdAt).getTime(); + const updated = new Date(updatedAt).getTime(); + return updated - created > 1000; +} + +function transformReply( + raw: RawCommentRow, + memberMap: Map, + currentUserId: string | undefined, + isOwner: boolean +): CommentReply { + return { + id: raw.id, + content: raw.content, + content_rendered: renderMentions(raw.content, memberMap), + author: buildAuthor(raw.author_id, memberMap), + created_at: raw.created_at, + updated_at: raw.updated_at, + is_edited: isEdited(raw.created_at, raw.updated_at), + can_edit: currentUserId === raw.author_id, + can_delete: currentUserId === raw.author_id || isOwner, + }; +} + +function transformComments( + rawComments: RawCommentRow[], + memberMap: Map, + currentUserId: string | undefined, + isOwner: boolean +): Map { + const byMessage = new Map< + number, + { topLevel: RawCommentRow[]; replies: Map } + >(); + + for (const raw of rawComments) { + if (!byMessage.has(raw.message_id)) { + byMessage.set(raw.message_id, { topLevel: [], replies: new Map() }); + } + const group = byMessage.get(raw.message_id)!; + + if (raw.parent_id === null) { + group.topLevel.push(raw); + } else { + if (!group.replies.has(raw.parent_id)) { + group.replies.set(raw.parent_id, []); + } + group.replies.get(raw.parent_id)!.push(raw); + } + } + + const result = new Map(); + + for (const [messageId, group] of byMessage) { + const comments: Comment[] = group.topLevel.map((raw) => { + const replies = (group.replies.get(raw.id) || []) + .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) + .map((r) => transformReply(r, memberMap, currentUserId, isOwner)); + + return { + id: raw.id, + message_id: raw.message_id, + content: raw.content, + content_rendered: renderMentions(raw.content, memberMap), + author: buildAuthor(raw.author_id, memberMap), + created_at: raw.created_at, + updated_at: raw.updated_at, + is_edited: isEdited(raw.created_at, raw.updated_at), + can_edit: currentUserId === raw.author_id, + can_delete: currentUserId === raw.author_id || isOwner, + reply_count: replies.length, + replies, + }; + }); + + comments.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + result.set(messageId, comments); + } + + return result; +} + +/** + * Syncs comments for a thread via Zero real-time sync. + * + * Syncs ALL comments for a thread in ONE subscription, then updates + * React Query cache for each message. This avoids N subscriptions for N messages. + */ +export function useCommentsSync(threadId: number | null) { + const queryClient = useQueryClient(); + + const { data: membersData } = useAtomValue(membersAtom); + const { data: currentUser } = useAtomValue(currentUserAtom); + const { data: myAccess } = useAtomValue(myAccessAtom); + + const memberMap = useMemo(() => buildMemberMap(membersData), [membersData]); + const currentUserId = currentUser?.id; + const isOwner = myAccess?.is_owner ?? false; + + const memberMapRef = useRef(memberMap); + const currentUserIdRef = useRef(currentUserId); + const isOwnerRef = useRef(isOwner); + const queryClientRef = useRef(queryClient); + + useEffect(() => { + memberMapRef.current = memberMap; + currentUserIdRef.current = currentUserId; + isOwnerRef.current = isOwner; + queryClientRef.current = queryClient; + }, [memberMap, currentUserId, isOwner, queryClient]); + + const updateReactQueryCache = useCallback((rows: RawCommentRow[]) => { + const commentsByMessage = transformComments( + rows, + memberMapRef.current, + currentUserIdRef.current, + isOwnerRef.current + ); + + for (const [messageId, comments] of commentsByMessage) { + const cacheKey = cacheKeys.comments.byMessage(messageId); + queryClientRef.current.setQueryData(cacheKey, { + comments, + total_count: comments.length, + }); + } + }, []); + + const [data] = useQuery(queries.comments.byThread({ threadId: threadId ?? -1 })); + + useEffect(() => { + if (!threadId || !data) return; + + const rows: RawCommentRow[] = data.map((c) => ({ + id: c.id, + message_id: c.messageId, + thread_id: c.threadId, + parent_id: c.parentId ?? null, + author_id: c.authorId ?? null, + content: c.content, + created_at: new Date(c.createdAt).toISOString(), + updated_at: new Date(c.updatedAt).toISOString(), + })); + + updateReactQueryCache(rows); + }, [threadId, data, updateReactQueryCache]); +} diff --git a/surfsense_web/hooks/use-connectors-electric.ts b/surfsense_web/hooks/use-connectors-electric.ts deleted file mode 100644 index 883186c3c..000000000 --- a/surfsense_web/hooks/use-connectors-electric.ts +++ /dev/null @@ -1,218 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useRef, useState } from "react"; -import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import type { SyncHandle } from "@/lib/electric/client"; -import { useElectricClient } from "@/lib/electric/context"; - -const IS_DEV = process.env.NODE_ENV === "development"; - -/** - * Hook for managing connectors with Electric SQL real-time sync - * - * Uses the Electric client from context (provided by ElectricProvider) - * instead of initializing its own - prevents race conditions and memory leaks - */ -export function useConnectorsElectric(searchSpaceId: number | string | null) { - // Get Electric client from context - ElectricProvider handles initialization - const electricClient = useElectricClient(); - - const [connectors, setConnectors] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const syncHandleRef = useRef(null); - const liveQueryRef = useRef<{ unsubscribe: () => void } | null>(null); - const syncKeyRef = useRef(null); - - // Transform connector data from Electric SQL/PGlite to match expected format - function transformConnector(connector: any): SearchSourceConnector { - return { - ...connector, - last_indexed_at: connector.last_indexed_at - ? typeof connector.last_indexed_at === "string" - ? connector.last_indexed_at - : new Date(connector.last_indexed_at).toISOString() - : null, - next_scheduled_at: connector.next_scheduled_at - ? typeof connector.next_scheduled_at === "string" - ? connector.next_scheduled_at - : new Date(connector.next_scheduled_at).toISOString() - : null, - created_at: connector.created_at - ? typeof connector.created_at === "string" - ? connector.created_at - : new Date(connector.created_at).toISOString() - : new Date().toISOString(), - }; - } - - // Start syncing when Electric client is available - useEffect(() => { - // If no Electric client available, immediately mark as not loading (disabled) - if (!electricClient) { - setLoading(false); - setError(new Error("Electric SQL not configured")); - return; - } - - // Wait for searchSpaceId to be available - if (!searchSpaceId) { - setConnectors([]); - setLoading(false); - return; - } - - // Create a unique key for this sync to prevent duplicate subscriptions - const syncKey = `connectors_${searchSpaceId}`; - if (syncKeyRef.current === syncKey) { - // Already syncing for this search space - return; - } - - let mounted = true; - syncKeyRef.current = syncKey; - - async function startSync() { - try { - if (IS_DEV) console.log("[useConnectorsElectric] Starting sync for search space:", searchSpaceId); - - const handle = await electricClient.syncShape({ - table: "search_source_connectors", - where: `search_space_id = ${searchSpaceId}`, - primaryKey: ["id"], - }); - - if (IS_DEV) console.log("[useConnectorsElectric] Sync started:", { - isUpToDate: handle.isUpToDate, - }); - - // Wait for initial sync with timeout - if (!handle.isUpToDate && handle.initialSyncPromise) { - try { - await Promise.race([ - handle.initialSyncPromise, - new Promise((resolve) => setTimeout(resolve, 2000)), - ]); - } catch (syncErr) { - console.error("[useConnectorsElectric] Initial sync failed:", syncErr); - } - } - - if (!mounted) { - handle.unsubscribe(); - return; - } - - syncHandleRef.current = handle; - setLoading(false); - setError(null); - - // Fetch initial connectors - await fetchConnectors(); - - // Set up live query for real-time updates - await setupLiveQuery(); - } catch (err) { - if (!mounted) return; - console.error("[useConnectorsElectric] Failed to start sync:", err); - setError(err instanceof Error ? err : new Error("Failed to sync connectors")); - setLoading(false); - } - } - - async function fetchConnectors() { - try { - const result = await electricClient.db.query( - `SELECT * FROM search_source_connectors WHERE search_space_id = $1 ORDER BY created_at DESC`, - [searchSpaceId] - ); - if (mounted) { - setConnectors((result.rows || []).map(transformConnector)); - } - } catch (err) { - console.error("[useConnectorsElectric] Failed to fetch:", err); - } - } - - async function setupLiveQuery() { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const db = electricClient.db as any; - - if (db.live?.query && typeof db.live.query === "function") { - const liveQuery = await db.live.query( - `SELECT * FROM search_source_connectors WHERE search_space_id = $1 ORDER BY created_at DESC`, - [searchSpaceId] - ); - - if (!mounted) { - liveQuery.unsubscribe?.(); - return; - } - - // Set initial results - if (liveQuery.initialResults?.rows) { - setConnectors(liveQuery.initialResults.rows.map(transformConnector)); - } else if (liveQuery.rows) { - setConnectors(liveQuery.rows.map(transformConnector)); - } - - // Subscribe to changes - if (typeof liveQuery.subscribe === "function") { - liveQuery.subscribe((result: { rows: any[] }) => { - if (mounted && result.rows) { - setConnectors(result.rows.map(transformConnector)); - } - }); - } - - if (typeof liveQuery.unsubscribe === "function") { - liveQueryRef.current = liveQuery; - } - } - } catch (liveErr) { - console.error("[useConnectorsElectric] Failed to set up live query:", liveErr); - } - } - - startSync(); - - return () => { - mounted = false; - syncKeyRef.current = null; - - if (syncHandleRef.current) { - try { - syncHandleRef.current.unsubscribe(); - } catch { - // PGlite may already be closed during cleanup - } - syncHandleRef.current = null; - } - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe(); - } catch { - // PGlite may already be closed during cleanup - } - liveQueryRef.current = null; - } - }; - }, [searchSpaceId, electricClient]); - - // Manual refresh function (optional, for fallback) - const refreshConnectors = useCallback(async () => { - if (!electricClient) return; - try { - const result = await electricClient.db.query( - `SELECT * FROM search_source_connectors WHERE search_space_id = $1 ORDER BY created_at DESC`, - [searchSpaceId] - ); - setConnectors((result.rows || []).map(transformConnector)); - } catch (err) { - console.error("[useConnectorsElectric] Failed to refresh:", err); - } - }, [electricClient, searchSpaceId]); - - return { connectors, loading, error, refreshConnectors }; -} diff --git a/surfsense_web/hooks/use-connectors-sync.ts b/surfsense_web/hooks/use-connectors-sync.ts new file mode 100644 index 000000000..d36728118 --- /dev/null +++ b/surfsense_web/hooks/use-connectors-sync.ts @@ -0,0 +1,43 @@ +"use client"; + +import { useQuery } from "@rocicorp/zero/react"; +import { useMemo } from "react"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import { queries } from "@/zero/queries"; + +/** + * Syncs connectors for a search space via Zero. + * Returns connectors, loading state, error, and a refresh function. + */ +export function useConnectorsSync(searchSpaceId: number | string | null) { + const spaceId = searchSpaceId ? Number(searchSpaceId) : -1; + + const [data, result] = useQuery(queries.connectors.bySpace({ searchSpaceId: spaceId })); + + const connectors: SearchSourceConnector[] = useMemo(() => { + if (!searchSpaceId || !data) return []; + return data.map((c) => ({ + id: c.id, + name: c.name, + connector_type: c.connectorType as SearchSourceConnector["connector_type"], + is_indexable: c.isIndexable, + is_active: true, + last_indexed_at: c.lastIndexedAt ? new Date(c.lastIndexedAt).toISOString() : null, + config: (c.config as Record) ?? {}, + enable_summary: c.enableSummary, + periodic_indexing_enabled: c.periodicIndexingEnabled, + indexing_frequency_minutes: c.indexingFrequencyMinutes ?? null, + next_scheduled_at: c.nextScheduledAt ? new Date(c.nextScheduledAt).toISOString() : null, + search_space_id: c.searchSpaceId, + user_id: c.userId, + created_at: c.createdAt ? new Date(c.createdAt).toISOString() : new Date().toISOString(), + })); + }, [searchSpaceId, data]); + + const loading = !searchSpaceId ? false : result.type !== "complete"; + const error = !searchSpaceId ? null : null; + + const refreshConnectors = async () => {}; + + return { connectors, loading, error, refreshConnectors }; +} diff --git a/surfsense_web/hooks/use-documents-processing.ts b/surfsense_web/hooks/use-documents-processing.ts index bb9901e64..e39c03de0 100644 --- a/surfsense_web/hooks/use-documents-processing.ts +++ b/surfsense_web/hooks/use-documents-processing.ts @@ -1,7 +1,8 @@ "use client"; +import { useQuery } from "@rocicorp/zero/react"; import { useEffect, useRef, useState } from "react"; -import { useElectricClient } from "@/lib/electric/context"; +import { queries } from "@/zero/queries"; export type DocumentsProcessingStatus = "idle" | "processing" | "success" | "error"; @@ -15,152 +16,64 @@ const SUCCESS_LINGER_MS = 5000; * - "idle" — nothing noteworthy (show normal icon) */ export function useDocumentsProcessing(searchSpaceId: number | null): DocumentsProcessingStatus { - const electricClient = useElectricClient(); const [status, setStatus] = useState("idle"); - const liveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null); const wasProcessingRef = useRef(false); const successTimerRef = useRef | null>(null); + const [documents] = useQuery(queries.documents.bySpace({ searchSpaceId: searchSpaceId ?? -1 })); + useEffect(() => { - if (!searchSpaceId || !electricClient) return; + if (!searchSpaceId || !documents) return; - const spaceId = searchSpaceId; - const client = electricClient; - let mounted = true; + let processingCount = 0; + let failedCount = 0; - async function setup() { - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may be closed */ - } - liveQueryRef.current = null; - } - - try { - const handle = await client.syncShape({ - table: "documents", - where: `search_space_id = ${spaceId}`, - columns: [ - "id", - "document_type", - "search_space_id", - "title", - "created_by_id", - "created_at", - "status", - ], - primaryKey: ["id"], - }); - - if (!mounted) return; - - if (!handle.isUpToDate && handle.initialSyncPromise) { - await Promise.race([ - handle.initialSyncPromise, - new Promise((resolve) => setTimeout(resolve, 5000)), - ]); - } - - if (!mounted) return; - - const db = client.db as { - live?: { - query: ( - sql: string, - params?: (number | string)[] - ) => Promise<{ - subscribe: (cb: (result: { rows: T[] }) => void) => void; - unsubscribe?: () => void; - }>; - }; - }; - - if (!db.live?.query) return; - - const liveQuery = await db.live.query<{ - processing_count: number | string; - failed_count: number | string; - }>( - `SELECT - SUM(CASE WHEN status->>'state' IN ('pending', 'processing') THEN 1 ELSE 0 END) AS processing_count, - SUM(CASE WHEN status->>'state' = 'failed' THEN 1 ELSE 0 END) AS failed_count - FROM documents - WHERE search_space_id = $1`, - [spaceId] - ); - - if (!mounted) { - liveQuery.unsubscribe?.(); - return; - } - - liveQuery.subscribe( - (result: { - rows: Array<{ processing_count: number | string; failed_count: number | string }>; - }) => { - if (!mounted || !result.rows?.[0]) return; - - const processingCount = Number(result.rows[0].processing_count) || 0; - const failedCount = Number(result.rows[0].failed_count) || 0; - - if (processingCount > 0) { - wasProcessingRef.current = true; - if (successTimerRef.current) { - clearTimeout(successTimerRef.current); - successTimerRef.current = null; - } - setStatus("processing"); - } else if (failedCount > 0) { - wasProcessingRef.current = false; - if (successTimerRef.current) { - clearTimeout(successTimerRef.current); - successTimerRef.current = null; - } - setStatus("error"); - } else if (wasProcessingRef.current) { - wasProcessingRef.current = false; - setStatus("success"); - if (successTimerRef.current) { - clearTimeout(successTimerRef.current); - } - successTimerRef.current = setTimeout(() => { - if (mounted) { - setStatus("idle"); - successTimerRef.current = null; - } - }, SUCCESS_LINGER_MS); - } else { - setStatus("idle"); - } - } - ); - - liveQueryRef.current = liveQuery; - } catch (err) { - console.error("[useDocumentsProcessing] Electric setup failed:", err); + for (const doc of documents) { + const state = (doc.status as { state?: string } | null)?.state; + if (state === "pending" || state === "processing") { + processingCount++; + } else if (state === "failed") { + failedCount++; } } - setup(); - - return () => { - mounted = false; + if (processingCount > 0) { + wasProcessingRef.current = true; if (successTimerRef.current) { clearTimeout(successTimerRef.current); successTimerRef.current = null; } - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may be closed */ - } - liveQueryRef.current = null; + setStatus("processing"); + } else if (failedCount > 0) { + wasProcessingRef.current = false; + if (successTimerRef.current) { + clearTimeout(successTimerRef.current); + successTimerRef.current = null; + } + setStatus("error"); + } else if (wasProcessingRef.current) { + wasProcessingRef.current = false; + setStatus("success"); + if (successTimerRef.current) { + clearTimeout(successTimerRef.current); + } + successTimerRef.current = setTimeout(() => { + setStatus("idle"); + successTimerRef.current = null; + }, SUCCESS_LINGER_MS); + } else { + setStatus("idle"); + } + }, [searchSpaceId, documents]); + + useEffect(() => { + return () => { + if (successTimerRef.current) { + clearTimeout(successTimerRef.current); + successTimerRef.current = null; } }; - }, [searchSpaceId, electricClient]); + }, []); return status; } diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 5fee85d01..df782ca83 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -1,27 +1,16 @@ "use client"; +import { useQuery } from "@rocicorp/zero/react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { DocumentSortBy, DocumentTypeEnum, SortOrder } from "@/contracts/types/document.types"; import { documentsApiService } from "@/lib/apis/documents-api.service"; -import { filterNewElectricItems, getNewestTimestamp } from "@/lib/electric/baseline"; -import type { SyncHandle } from "@/lib/electric/client"; -import { useElectricClient } from "@/lib/electric/context"; +import { queries } from "@/zero/queries"; export interface DocumentStatusType { state: "ready" | "pending" | "processing" | "failed"; reason?: string; } -interface DocumentElectric { - id: number; - search_space_id: number; - document_type: string; - title: string; - created_by_id: string | null; - created_at: string; - status: DocumentStatusType | null; -} - export interface DocumentDisplay { id: number; search_space_id: number; @@ -64,23 +53,14 @@ const EMPTY_TYPE_FILTER: DocumentTypeEnum[] = []; const INITIAL_PAGE_SIZE = 50; const SCROLL_PAGE_SIZE = 5; -function isValidDocument(doc: DocumentElectric): boolean { - return doc.id != null && doc.title != null && doc.title !== ""; -} - /** - * Paginated documents hook with Electric SQL real-time updates. + * Paginated documents hook with Zero real-time updates. * * Architecture: * 1. API is the PRIMARY data source — fetches pages on demand * 2. Type counts come from a dedicated lightweight API endpoint - * 3. Electric provides REAL-TIME updates (new docs, deletions, status changes) + * 3. Zero provides REAL-TIME updates (new docs, deletions, status changes) * 4. Server-side sorting via sort_by + sort_order params - * - * @param searchSpaceId - The search space to load documents for - * @param typeFilter - Document types to filter by (server-side) - * @param sortBy - Column to sort by (server-side) - * @param sortOrder - Sort direction (server-side) */ export function useDocuments( searchSpaceId: number | null, @@ -88,8 +68,6 @@ export function useDocuments( sortBy: DocumentSortBy = "created_at", sortOrder: SortOrder = "desc" ) { - const electricClient = useElectricClient(); - const [documents, setDocuments] = useState([]); const [typeCounts, setTypeCounts] = useState>({}); const [total, setTotal] = useState(0); @@ -103,14 +81,8 @@ export function useDocuments( const prevParamsRef = useRef<{ sortBy: string; sortOrder: string; typeFilterKey: string } | null>( null ); - // Snapshot of all doc IDs from Electric's first callback after initial load. - // Anything appearing in subsequent callbacks NOT in this set is genuinely new. - const electricBaselineIdsRef = useRef | null>(null); - const newestApiTimestampRef = useRef(null); const userCacheRef = useRef>(new Map()); const emailCacheRef = useRef>(new Map()); - const syncHandleRef = useRef(null); - const liveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null); const typeFilterKey = typeFilter.join(","); @@ -141,20 +113,6 @@ export function useDocuments( [] ); - const electricToDisplayDoc = useCallback( - (doc: DocumentElectric): DocumentDisplay => ({ - ...doc, - created_by_name: doc.created_by_id - ? (userCacheRef.current.get(doc.created_by_id) ?? null) - : null, - created_by_email: doc.created_by_id - ? (emailCacheRef.current.get(doc.created_by_id) ?? null) - : null, - status: doc.status ?? { state: "ready" }, - }), - [] - ); - // EFFECT 1: Fetch first page + type counts when params change // biome-ignore lint/correctness/useExhaustiveDependencies: typeFilterKey serializes typeFilter useEffect(() => { @@ -178,8 +136,6 @@ export function useDocuments( } apiLoadedCountRef.current = 0; initialLoadDoneRef.current = false; - electricBaselineIdsRef.current = null; - newestApiTimestampRef.current = null; const fetchInitialData = async () => { try { @@ -209,7 +165,6 @@ export function useDocuments( setTypeCounts(countsResponse); setError(null); apiLoadedCountRef.current = docsResponse.items.length; - newestApiTimestampRef.current = getNewestTimestamp(docsResponse.items); initialLoadDoneRef.current = true; } catch (err) { if (cancelled) return; @@ -226,207 +181,106 @@ export function useDocuments( }; }, [searchSpaceId, typeFilterKey, sortBy, sortOrder, populateUserCache, apiToDisplayDoc]); - // EFFECT 2: Electric sync + live query for real-time updates + // EFFECT 2: Zero real-time sync for document updates + const [zeroDocuments] = useQuery( + queries.documents.bySpace({ searchSpaceId: searchSpaceId ?? -1 }) + ); + useEffect(() => { - if (!searchSpaceId || !electricClient) return; + if (!searchSpaceId || !zeroDocuments || !initialLoadDoneRef.current) return; - const spaceId = searchSpaceId; - const client = electricClient; - let mounted = true; + const validItems = zeroDocuments.filter( + (doc) => doc.id != null && doc.title != null && doc.title !== "" + ); - async function setupElectricRealtime() { - if (syncHandleRef.current) { - try { - syncHandleRef.current.unsubscribe(); - } catch { - /* PGlite may already be closed */ - } - syncHandleRef.current = null; - } - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may already be closed */ - } - liveQueryRef.current = null; - } + const unknownUserIds = validItems.filter( + (doc) => doc.createdById !== null && !userCacheRef.current.has(doc.createdById!) + ); - try { - const handle = await client.syncShape({ - table: "documents", - where: `search_space_id = ${spaceId}`, - columns: [ - "id", - "document_type", - "search_space_id", - "title", - "created_by_id", - "created_at", - "status", - ], - primaryKey: ["id"], - }); - - if (!mounted) { - handle.unsubscribe(); - return; - } - - syncHandleRef.current = handle; - - if (!handle.isUpToDate && handle.initialSyncPromise) { - await Promise.race([ - handle.initialSyncPromise, - new Promise((resolve) => setTimeout(resolve, 5000)), - ]); - } - - if (!mounted) return; - - const db = client.db as { - live?: { - query: ( - sql: string, - params?: (number | string)[] - ) => Promise<{ - subscribe: (cb: (result: { rows: T[] }) => void) => void; - unsubscribe?: () => void; - }>; - }; - }; - - if (!db.live?.query) return; - - const query = `SELECT id, document_type, search_space_id, title, created_by_id, created_at, status - FROM documents - WHERE search_space_id = $1 - ORDER BY created_at DESC`; - - const liveQuery = await db.live.query(query, [spaceId]); - - if (!mounted) { - liveQuery.unsubscribe?.(); - return; - } - - liveQuery.subscribe((result: { rows: DocumentElectric[] }) => { - if (!mounted || !result.rows || !initialLoadDoneRef.current) return; - - const validItems = result.rows.filter(isValidDocument); - const isFullySynced = syncHandleRef.current?.isUpToDate ?? false; - - const unknownUserIds = validItems - .filter( - (doc): doc is DocumentElectric & { created_by_id: string } => - doc.created_by_id !== null && !userCacheRef.current.has(doc.created_by_id) - ) - .map((doc) => doc.created_by_id); - - if (unknownUserIds.length > 0) { - documentsApiService - .getDocuments({ - queryParams: { - search_space_id: spaceId, - page: 0, - page_size: 20, - }, - }) - .then((response) => { - populateUserCache(response.items); - if (mounted) { - setDocuments((prev) => - prev.map((doc) => ({ - ...doc, - created_by_name: doc.created_by_id - ? (userCacheRef.current.get(doc.created_by_id) ?? null) - : null, - created_by_email: doc.created_by_id - ? (emailCacheRef.current.get(doc.created_by_id) ?? null) - : null, - })) - ); - } - }) - .catch(() => {}); - } - - setDocuments((prev) => { - const liveIds = new Set(validItems.map((d) => d.id)); - const prevIds = new Set(prev.map((d) => d.id)); - - const newItems = filterNewElectricItems( - validItems, - liveIds, - prevIds, - electricBaselineIdsRef, - newestApiTimestampRef.current - ).map(electricToDisplayDoc); - - // Update existing docs (status changes, title edits) - let updated = prev.map((doc) => { - if (liveIds.has(doc.id)) { - const liveItem = validItems.find((v) => v.id === doc.id); - if (liveItem) { - return electricToDisplayDoc(liveItem); - } - } - return doc; - }); - - // Remove deleted docs (only when fully synced) - if (isFullySynced) { - updated = updated.filter((doc) => liveIds.has(doc.id)); - } - - if (newItems.length > 0) { - return [...newItems, ...updated]; - } - - return updated; - }); - - // Update type counts when Electric detects changes - if (isFullySynced && validItems.length > 0) { - const counts: Record = {}; - for (const item of validItems) { - counts[item.document_type] = (counts[item.document_type] || 0) + 1; - } - setTypeCounts(counts); - setTotal(validItems.length); - } - }); - - liveQueryRef.current = liveQuery; - } catch (err) { - console.error("[useDocuments] Electric setup failed:", err); - } + if (unknownUserIds.length > 0) { + documentsApiService + .getDocuments({ + queryParams: { + search_space_id: searchSpaceId, + page: 0, + page_size: 20, + }, + }) + .then((response) => { + populateUserCache(response.items); + setDocuments((prev) => + prev.map((doc) => ({ + ...doc, + created_by_name: doc.created_by_id + ? (userCacheRef.current.get(doc.created_by_id) ?? null) + : null, + created_by_email: doc.created_by_id + ? (emailCacheRef.current.get(doc.created_by_id) ?? null) + : null, + })) + ); + }) + .catch(() => {}); } - setupElectricRealtime(); + const liveIds = new Set(validItems.map((d) => d.id)); - return () => { - mounted = false; - if (syncHandleRef.current) { - try { - syncHandleRef.current.unsubscribe(); - } catch { - /* PGlite may already be closed */ - } - syncHandleRef.current = null; - } - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may already be closed */ - } - liveQueryRef.current = null; - } - }; - }, [searchSpaceId, electricClient, electricToDisplayDoc, populateUserCache]); + setDocuments((prev) => { + const prevIds = new Set(prev.map((d) => d.id)); - // Reset on search space change + const newItems: DocumentDisplay[] = validItems + .filter((d) => !prevIds.has(d.id)) + .map((doc) => ({ + id: doc.id, + search_space_id: doc.searchSpaceId, + document_type: doc.documentType, + title: doc.title, + created_by_id: doc.createdById ?? null, + created_by_name: doc.createdById + ? (userCacheRef.current.get(doc.createdById) ?? null) + : null, + created_by_email: doc.createdById + ? (emailCacheRef.current.get(doc.createdById) ?? null) + : null, + created_at: new Date(doc.createdAt).toISOString(), + status: (doc.status as unknown as DocumentStatusType) ?? { state: "ready" }, + })); + + let updated = prev.map((existing) => { + if (liveIds.has(existing.id)) { + const liveItem = validItems.find((v) => v.id === existing.id); + if (liveItem) { + return { + ...existing, + title: liveItem.title, + document_type: liveItem.documentType, + status: (liveItem.status as unknown as DocumentStatusType) ?? { + state: "ready" as const, + }, + }; + } + } + return existing; + }); + + updated = updated.filter((doc) => liveIds.has(doc.id)); + + if (newItems.length > 0) { + return [...newItems, ...updated]; + } + + return updated; + }); + + const counts: Record = {}; + for (const item of validItems) { + counts[item.documentType] = (counts[item.documentType] || 0) + 1; + } + setTypeCounts(counts); + setTotal(validItems.length); + }, [searchSpaceId, zeroDocuments, populateUserCache]); + + // EFFECT 3: Reset on search space change const prevSearchSpaceIdRef = useRef(null); useEffect(() => { @@ -437,8 +291,6 @@ export function useDocuments( setHasMore(false); apiLoadedCountRef.current = 0; initialLoadDoneRef.current = false; - electricBaselineIdsRef.current = null; - newestApiTimestampRef.current = null; userCacheRef.current.clear(); emailCacheRef.current.clear(); } diff --git a/surfsense_web/hooks/use-inbox.ts b/surfsense_web/hooks/use-inbox.ts index f301dc90e..4203c3506 100644 --- a/surfsense_web/hooks/use-inbox.ts +++ b/surfsense_web/hooks/use-inbox.ts @@ -1,10 +1,10 @@ "use client"; +import { useQuery } from "@rocicorp/zero/react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { InboxItem, NotificationCategory } from "@/contracts/types/inbox.types"; import { notificationsApiService } from "@/lib/apis/notifications-api.service"; -import { filterNewElectricItems, getNewestTimestamp } from "@/lib/electric/baseline"; -import { useElectricClient } from "@/lib/electric/context"; +import { queries } from "@/zero/queries"; export type { InboxItem, @@ -16,17 +16,16 @@ const INITIAL_PAGE_SIZE = 50; const SCROLL_PAGE_SIZE = 30; const SYNC_WINDOW_DAYS = 4; -const CATEGORY_TYPE_SQL: Record = { - comments: "AND type IN ('new_mention', 'comment_reply')", - status: - "AND type IN ('connector_indexing', 'connector_deletion', 'document_processing', 'page_limit_exceeded')", +const CATEGORY_TYPES: Record = { + comments: ["new_mention", "comment_reply"], + status: [ + "connector_indexing", + "connector_deletion", + "document_processing", + "page_limit_exceeded", + ], }; -/** - * Calculate the cutoff date for sync window. - * Rounds to the start of the day (midnight UTC) to ensure stable values - * across re-renders. - */ function getSyncCutoffDate(): string { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - SYNC_WINDOW_DAYS); @@ -35,24 +34,12 @@ function getSyncCutoffDate(): string { } /** - * Hook for managing inbox items with API-first architecture + Electric real-time deltas. + * Hook for managing inbox items with API-first architecture + Zero real-time deltas. * - * Architecture (Documents pattern, per-tab): + * Architecture: * 1. API is the PRIMARY data source — fetches first page on mount with category filter - * 2. Electric provides REAL-TIME updates (new items, status changes, read state) - * 3. Baseline pattern prevents duplicates between API and Electric - * 4. Electric sync shape is SHARED across instances (client-level caching) - * — each instance creates its own type-filtered live queries - * - * Unread count strategy: - * - API provides the category-filtered total on mount (ground truth across all time) - * - Electric live query counts unread within SYNC_WINDOW_DAYS (filtered by type) - * - olderUnreadOffsetRef bridges the gap: total = offset + recent - * - Optimistic updates adjust both the count and the offset (for old items) - * - * @param userId - The user ID to fetch inbox items for - * @param searchSpaceId - The search space ID to filter inbox items - * @param category - Which tab: "comments" or "status" + * 2. Zero provides REAL-TIME updates (new items, status changes, read state) + * 3. Unread count = olderUnreadOffset + recent unread from Zero */ export function useInbox( userId: string | null, @@ -61,8 +48,6 @@ export function useInbox( prefetchedUnread?: { total_unread: number; recent_unread: number } | null, prefetchedUnreadReady = true ) { - const electricClient = useElectricClient(); - const [inboxItems, setInboxItems] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); @@ -71,17 +56,12 @@ export function useInbox( const [unreadCount, setUnreadCount] = useState(0); const initialLoadDoneRef = useRef(false); - const electricBaselineIdsRef = useRef | null>(null); - const newestApiTimestampRef = useRef(null); - const liveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null); - const unreadLiveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null); - const olderUnreadOffsetRef = useRef(null); const apiUnreadTotalRef = useRef(0); - // EFFECT 1: Fetch first page + unread count from API with category filter. - // When prefetchedUnreadReady=false, we wait for the batch query to settle - // before deciding whether we need an individual unread-count fallback call. + const categoryTypes = CATEGORY_TYPES[category]; + + // EFFECT 1: Fetch first page + unread count from API with category filter useEffect(() => { if (!userId || !searchSpaceId) return; if (!prefetchedUnreadReady) return; @@ -92,8 +72,6 @@ export function useInbox( setInboxItems([]); setHasMore(false); initialLoadDoneRef.current = false; - electricBaselineIdsRef.current = null; - newestApiTimestampRef.current = null; olderUnreadOffsetRef.current = null; apiUnreadTotalRef.current = 0; @@ -107,7 +85,6 @@ export function useInbox( }, }); - // Use prefetched counts when available, otherwise fetch individually. const unreadPromise = prefetchedUnread ? Promise.resolve(prefetchedUnread) : notificationsApiService.getUnreadCount(searchSpaceId, undefined, category); @@ -123,7 +100,6 @@ export function useInbox( setHasMore(notificationsResponse.has_more); setUnreadCount(unreadResponse.total_unread); apiUnreadTotalRef.current = unreadResponse.total_unread; - newestApiTimestampRef.current = getNewestTimestamp(notificationsResponse.items); setError(null); initialLoadDoneRef.current = true; } catch (err) { @@ -141,208 +117,83 @@ export function useInbox( }; }, [userId, searchSpaceId, category, prefetchedUnread, prefetchedUnreadReady]); - // EFFECT 2: Electric sync (shared shape) + per-instance type-filtered live queries + // EFFECT 2: Zero real-time sync for notification updates + const [zeroNotifications] = useQuery(queries.notifications.byUser({ userId: userId ?? "" })); + useEffect(() => { - if (!userId || !searchSpaceId || !electricClient) return; + if (!userId || !searchSpaceId || !zeroNotifications || !initialLoadDoneRef.current) return; - const uid = userId; - const spaceId = searchSpaceId; - const client = electricClient; - const typeFilter = CATEGORY_TYPE_SQL[category]; - let mounted = true; + const cutoff = new Date(getSyncCutoffDate()); - async function setupElectricRealtime() { - // Clean up previous live queries (NOT the sync shape — it's shared) - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may be closed */ + const validItems = zeroNotifications.filter((item) => { + if (item.id == null) return false; + if (!categoryTypes.includes(item.type)) return false; + if (item.searchSpaceId !== null && item.searchSpaceId !== searchSpaceId) return false; + return true; + }); + + const recentItems = validItems.filter((item) => new Date(item.createdAt) > cutoff); + + const liveIds = new Set(recentItems.map((d) => d.id)); + + setInboxItems((prev) => { + const prevIds = new Set(prev.map((d) => d.id)); + + const newItems: InboxItem[] = recentItems + .filter((d) => !prevIds.has(d.id)) + .map( + (item) => + ({ + id: item.id, + user_id: item.userId, + search_space_id: item.searchSpaceId ?? undefined, + type: item.type, + title: item.title, + message: item.message, + read: item.read, + metadata: item.metadata as unknown as Record, + created_at: new Date(item.createdAt).toISOString(), + updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : null, + }) as InboxItem + ); + + let updated = prev.map((existing) => { + const liveItem = recentItems.find((v) => v.id === existing.id); + if (liveItem) { + return { + ...existing, + read: liveItem.read, + title: liveItem.title, + message: liveItem.message, + metadata: liveItem.metadata as unknown as Record, + } as InboxItem; } - liveQueryRef.current = null; - } - if (unreadLiveQueryRef.current) { - try { - unreadLiveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may be closed */ - } - unreadLiveQueryRef.current = null; + return existing; + }); + + updated = updated.filter((item) => { + if (new Date(item.created_at) < cutoff) return true; + return liveIds.has(item.id); + }); + + if (newItems.length > 0) { + return [...newItems, ...updated]; } - try { - const cutoffDate = getSyncCutoffDate(); + return updated; + }); - // Sync shape is cached by the Electric client — multiple hook instances - // calling syncShape with the same params get the same handle. - const handle = await client.syncShape({ - table: "notifications", - where: `user_id = '${uid}' AND created_at > '${cutoffDate}'`, - primaryKey: ["id"], - }); - - if (!mounted) return; - - if (!handle.isUpToDate && handle.initialSyncPromise) { - await Promise.race([ - handle.initialSyncPromise, - new Promise((resolve) => setTimeout(resolve, 5000)), - ]); - } - - if (!mounted) return; - - const db = client.db as { - live?: { - query: ( - sql: string, - params?: (number | string)[] - ) => Promise<{ - subscribe: (cb: (result: { rows: T[] }) => void) => void; - unsubscribe?: () => void; - }>; - }; - }; - - if (!db.live?.query) return; - - // Per-instance live query filtered by category types - const itemsQuery = `SELECT * FROM notifications - WHERE user_id = $1 - AND (search_space_id = $2 OR search_space_id IS NULL) - AND created_at > '${cutoffDate}' - ${typeFilter} - ORDER BY created_at DESC`; - - const liveQuery = await db.live.query(itemsQuery, [uid, spaceId]); - - if (!mounted) { - liveQuery.unsubscribe?.(); - return; - } - - liveQuery.subscribe((result: { rows: InboxItem[] }) => { - if (!mounted || !result.rows || !initialLoadDoneRef.current) return; - - const validItems = result.rows.filter((item) => item.id != null && item.title != null); - const cutoff = new Date(getSyncCutoffDate()); - - const liveItemMap = new Map(validItems.map((d) => [d.id, d])); - const liveIds = new Set(liveItemMap.keys()); - - setInboxItems((prev) => { - const prevIds = new Set(prev.map((d) => d.id)); - - const newItems = filterNewElectricItems( - validItems, - liveIds, - prevIds, - electricBaselineIdsRef, - newestApiTimestampRef.current - ); - - let updated = prev.map((item) => { - const liveItem = liveItemMap.get(item.id); - if (liveItem) return liveItem; - return item; - }); - - const isFullySynced = handle.isUpToDate; - if (isFullySynced) { - updated = updated.filter((item) => { - if (new Date(item.created_at) < cutoff) return true; - return liveIds.has(item.id); - }); - } - - if (newItems.length > 0) { - return [...newItems, ...updated]; - } - - return updated; - }); - - // Calibrate the older-unread offset using baseline items - // (items present in both Electric and the API-loaded list). - // This avoids the timing bug where new items arriving between - // the API fetch and Electric's first callback would be absorbed - // into the offset, making the count appear unchanged. - const baseline = electricBaselineIdsRef.current; - if (olderUnreadOffsetRef.current === null && baseline !== null) { - const baselineUnreadCount = validItems.filter( - (item) => baseline.has(item.id) && !item.read - ).length; - olderUnreadOffsetRef.current = Math.max( - 0, - apiUnreadTotalRef.current - baselineUnreadCount - ); - } - - // Derive unread count from all Electric items + the older offset - if (olderUnreadOffsetRef.current !== null) { - const electricUnreadCount = validItems.filter((item) => !item.read).length; - setUnreadCount(olderUnreadOffsetRef.current + electricUnreadCount); - } - }); - - liveQueryRef.current = liveQuery; - - // Per-instance unread count live query filtered by category types. - // Acts as a secondary reactive path for read-status changes that - // may not trigger the items live query in all edge cases. - const countQuery = `SELECT COUNT(*) as count FROM notifications - WHERE user_id = $1 - AND (search_space_id = $2 OR search_space_id IS NULL) - AND created_at > '${cutoffDate}' - AND read = false - ${typeFilter}`; - - const countLiveQuery = await db.live.query<{ count: number | string }>(countQuery, [ - uid, - spaceId, - ]); - - if (!mounted) { - countLiveQuery.unsubscribe?.(); - return; - } - - countLiveQuery.subscribe((result: { rows: Array<{ count: number | string }> }) => { - if (!mounted || !result.rows?.[0] || !initialLoadDoneRef.current) return; - if (olderUnreadOffsetRef.current === null) return; - const liveRecentUnread = Number(result.rows[0].count) || 0; - setUnreadCount(olderUnreadOffsetRef.current + liveRecentUnread); - }); - - unreadLiveQueryRef.current = countLiveQuery; - } catch (err) { - console.error(`[useInbox:${category}] Electric setup failed:`, err); - } + // Calibrate older-unread offset on first Zero data + if (olderUnreadOffsetRef.current === null) { + const recentUnreadCount = recentItems.filter((item) => !item.read).length; + olderUnreadOffsetRef.current = Math.max(0, apiUnreadTotalRef.current - recentUnreadCount); } - setupElectricRealtime(); - - return () => { - mounted = false; - // Only clean up live queries — sync shape is shared across instances - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may be closed */ - } - liveQueryRef.current = null; - } - if (unreadLiveQueryRef.current) { - try { - unreadLiveQueryRef.current.unsubscribe?.(); - } catch { - /* PGlite may be closed */ - } - unreadLiveQueryRef.current = null; - } - }; - }, [userId, searchSpaceId, electricClient, category]); + if (olderUnreadOffsetRef.current !== null) { + const recentUnreadCount = recentItems.filter((item) => !item.read).length; + setUnreadCount(olderUnreadOffsetRef.current + recentUnreadCount); + } + }, [userId, searchSpaceId, zeroNotifications, categoryTypes]); // Load more pages via API (cursor-based using before_date) const loadMore = useCallback(async () => { diff --git a/surfsense_web/hooks/use-messages-electric.ts b/surfsense_web/hooks/use-messages-electric.ts deleted file mode 100644 index 4928bed63..000000000 --- a/surfsense_web/hooks/use-messages-electric.ts +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useRef } from "react"; -import type { RawMessage } from "@/contracts/types/chat-messages.types"; -import type { SyncHandle } from "@/lib/electric/client"; -import { useElectricClient } from "@/lib/electric/context"; - -/** - * Syncs chat messages for a thread via Electric SQL. - * Calls onMessagesUpdate when messages change. - */ -export function useMessagesElectric( - threadId: number | null, - onMessagesUpdate: (messages: RawMessage[]) => void -) { - const electricClient = useElectricClient(); - - const syncHandleRef = useRef(null); - const liveQueryRef = useRef<{ unsubscribe: () => void } | null>(null); - const syncKeyRef = useRef(null); - const onMessagesUpdateRef = useRef(onMessagesUpdate); - - useEffect(() => { - onMessagesUpdateRef.current = onMessagesUpdate; - }, [onMessagesUpdate]); - - const handleMessagesUpdate = useCallback((rows: RawMessage[]) => { - onMessagesUpdateRef.current(rows); - }, []); - - useEffect(() => { - if (!threadId || !electricClient) { - return; - } - - const syncKey = `messages_${threadId}`; - if (syncKeyRef.current === syncKey) { - return; - } - - const client = electricClient; - let mounted = true; - syncKeyRef.current = syncKey; - - async function startSync() { - try { - const handle = await client.syncShape({ - table: "new_chat_messages", - where: `thread_id = ${threadId}`, - columns: ["id", "thread_id", "role", "content", "author_id", "created_at"], - primaryKey: ["id"], - }); - - if (!handle.isUpToDate && handle.initialSyncPromise) { - try { - await Promise.race([ - handle.initialSyncPromise, - new Promise((resolve) => setTimeout(resolve, 3000)), - ]); - } catch (err) { - console.warn("[useMessagesElectric] Sync timeout:", err); - } - } - - if (!mounted) { - handle.unsubscribe(); - return; - } - - syncHandleRef.current = handle; - await fetchMessages(); - await setupLiveQuery(); - } catch (err) { - console.warn("[useMessagesElectric] Sync failed:", err); - } - } - - async function fetchMessages() { - try { - const result = await client.db.query( - `SELECT id, thread_id, role, content, author_id, created_at - FROM new_chat_messages - WHERE thread_id = $1 - ORDER BY created_at ASC`, - [threadId] - ); - - if (mounted && result.rows) { - handleMessagesUpdate(result.rows); - } - } catch (err) { - console.warn("[useMessagesElectric] Query failed:", err); - } - } - - async function setupLiveQuery() { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const db = client.db as any; - - if (db.live?.query && typeof db.live.query === "function") { - const liveQuery = await db.live.query( - `SELECT id, thread_id, role, content, author_id, created_at - FROM new_chat_messages - WHERE thread_id = $1 - ORDER BY created_at ASC`, - [threadId] - ); - - if (!mounted) { - liveQuery.unsubscribe?.(); - return; - } - - if (liveQuery.initialResults?.rows) { - handleMessagesUpdate(liveQuery.initialResults.rows); - } else if (liveQuery.rows) { - handleMessagesUpdate(liveQuery.rows); - } - - if (typeof liveQuery.subscribe === "function") { - liveQuery.subscribe((result: { rows: RawMessage[] }) => { - if (mounted && result.rows) { - handleMessagesUpdate(result.rows); - } - }); - } - - if (typeof liveQuery.unsubscribe === "function") { - liveQueryRef.current = liveQuery; - } - } - } catch (err) { - console.warn("[useMessagesElectric] Live query failed:", err); - } - } - - startSync(); - - return () => { - mounted = false; - syncKeyRef.current = null; - - if (syncHandleRef.current) { - try { - syncHandleRef.current.unsubscribe(); - } catch { - // PGlite may already be closed during cleanup - } - syncHandleRef.current = null; - } - if (liveQueryRef.current) { - try { - liveQueryRef.current.unsubscribe(); - } catch { - // PGlite may already be closed during cleanup - } - liveQueryRef.current = null; - } - }; - }, [threadId, electricClient, handleMessagesUpdate]); -} diff --git a/surfsense_web/hooks/use-messages-sync.ts b/surfsense_web/hooks/use-messages-sync.ts new file mode 100644 index 000000000..ddbe8a757 --- /dev/null +++ b/surfsense_web/hooks/use-messages-sync.ts @@ -0,0 +1,38 @@ +"use client"; + +import { useQuery } from "@rocicorp/zero/react"; +import { useEffect, useRef } from "react"; +import type { RawMessage } from "@/contracts/types/chat-messages.types"; +import { queries } from "@/zero/queries"; + +/** + * Syncs chat messages for a thread via Zero. + * Calls onMessagesUpdate when messages change. + */ +export function useMessagesSync( + threadId: number | null, + onMessagesUpdate: (messages: RawMessage[]) => void +) { + const onMessagesUpdateRef = useRef(onMessagesUpdate); + + useEffect(() => { + onMessagesUpdateRef.current = onMessagesUpdate; + }, [onMessagesUpdate]); + + const [messages] = useQuery(queries.messages.byThread({ threadId: threadId ?? -1 })); + + useEffect(() => { + if (!threadId || !messages) return; + + const mapped: RawMessage[] = messages.map((msg) => ({ + id: msg.id, + thread_id: msg.threadId, + role: msg.role, + content: msg.content, + author_id: msg.authorId ?? null, + created_at: new Date(msg.createdAt).toISOString(), + })); + + onMessagesUpdateRef.current(mapped); + }, [threadId, messages]); +} diff --git a/surfsense_web/lib/electric/auth.ts b/surfsense_web/lib/electric/auth.ts deleted file mode 100644 index 2b65ba091..000000000 --- a/surfsense_web/lib/electric/auth.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Get auth token for Electric SQL - * In production, this should get the token from your auth system - */ - -export async function getElectricAuthToken(): Promise { - // For insecure mode (development), return empty string - if (process.env.NEXT_PUBLIC_ELECTRIC_AUTH_MODE === "insecure") { - return ""; - } - - // In production, get token from your auth system - // This should match your backend auth token - if (typeof window !== "undefined") { - const token = localStorage.getItem("surfsense_bearer_token"); - return token || ""; - } - - return ""; -} diff --git a/surfsense_web/lib/electric/baseline.ts b/surfsense_web/lib/electric/baseline.ts deleted file mode 100644 index 6f819628e..000000000 --- a/surfsense_web/lib/electric/baseline.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { MutableRefObject } from "react"; - -/** - * Extract the newest `created_at` timestamp from a list of items. - * Used to establish the server-clock cutoff for the baseline timing-gap check. - * - * Uses Date parsing instead of string comparison because the API (Python - * isoformat: "+00:00" suffix) and Electric/PGlite ("Z" suffix, variable - * fractional-second precision) produce different string formats. - */ -export function getNewestTimestamp(items: T[]): string | null { - if (items.length === 0) return null; - let newest = items[0].created_at; - let newestMs = new Date(newest).getTime(); - for (let i = 1; i < items.length; i++) { - const ms = new Date(items[i].created_at).getTime(); - if (ms > newestMs) { - newest = items[i].created_at; - newestMs = ms; - } - } - return newest; -} - -/** - * Identify genuinely new items from an Electric live query callback. - * - * On Electric's first callback, ALL live IDs are snapshotted as the baseline. - * Items beyond the API's first page are in this baseline and stay hidden - * (they'll appear via scroll pagination). Items created in the timing gap - * between the API fetch and Electric's first callback are rescued via the - * `newestApiTimestamp` check — their `created_at` is newer than anything - * the API returned, so they pass through. - * - */ -export function filterNewElectricItems( - validItems: T[], - liveIds: Set, - prevIds: Set, - baselineRef: MutableRefObject | null>, - newestApiTimestamp: string | null -): T[] { - if (baselineRef.current === null) { - baselineRef.current = new Set(liveIds); - } - - const baseline = baselineRef.current; - const cutoffMs = newestApiTimestamp ? new Date(newestApiTimestamp).getTime() : null; - - const newItems = validItems.filter((item) => { - if (prevIds.has(item.id)) return false; - if (!baseline.has(item.id)) return true; - if (cutoffMs !== null && new Date(item.created_at).getTime() > cutoffMs) return true; - return false; - }); - - for (const item of newItems) { - baseline.add(item.id); - } - - return newItems; -} diff --git a/surfsense_web/lib/electric/client.ts b/surfsense_web/lib/electric/client.ts deleted file mode 100644 index 09ef5e300..000000000 --- a/surfsense_web/lib/electric/client.ts +++ /dev/null @@ -1,848 +0,0 @@ -/** - * Electric SQL client setup for ElectricSQL 1.x with PGlite - * - * USER-SPECIFIC DATABASE ARCHITECTURE: - * - Each user gets their own IndexedDB database: idb://surfsense-{userId}-v{version} - * - On login: cleanup databases from other users, then initialize current user's DB - * - On logout: best-effort cleanup (not relied upon) - * - * This ensures: - * 1. Complete user isolation (data can never leak between users) - * 2. Self-healing on login (stale databases are cleaned up) - * 3. Works even if logout cleanup fails - */ - -import { PGlite, type Transaction } from "@electric-sql/pglite"; -import { live } from "@electric-sql/pglite/live"; -import { electricSync } from "@electric-sql/pglite-sync"; - -// Debug logging - only logs in development, silent in production -const IS_DEV = process.env.NODE_ENV === "development"; - -function debugLog(...args: unknown[]) { - if (IS_DEV) console.log(...args); -} - -function debugWarn(...args: unknown[]) { - if (IS_DEV) console.warn(...args); -} - -// Types -export interface ElectricClient { - db: PGlite; - userId: string; - syncShape: (options: SyncShapeOptions) => Promise; -} - -export interface SyncShapeOptions { - table: string; - where?: string; - columns?: string[]; - primaryKey?: string[]; -} - -export interface SyncHandle { - unsubscribe: () => void; - readonly isUpToDate: boolean; - // The stream property contains the ShapeStreamInterface from pglite-sync - stream?: unknown; - // Promise that resolves when initial sync is complete - initialSyncPromise?: Promise; -} - -// Singleton state - now tracks the user ID -let electricClient: ElectricClient | null = null; -let currentUserId: string | null = null; -let isInitializing = false; -let initPromise: Promise | null = null; - -// Cache for sync handles to prevent duplicate subscriptions (memory optimization) -const activeSyncHandles = new Map(); - -// Track pending sync operations to prevent race conditions -// If a sync is in progress, subsequent calls will wait for it instead of starting a new one -const pendingSyncs = new Map>(); - -// Version for sync state - increment this to force fresh sync when Electric config changes -// v2: user-specific database architecture -// v3: consistent cutoff date for sync+queries, visibility refresh support -// v4: heartbeat-based stale notification detection with updated_at tracking -// v5: fixed duplicate key errors, stable cutoff dates, onMustRefetch handler, -// real-time documents table with title/created_by_id/status columns, -// consolidated single documents sync, pending state for document queue visibility -// v6: added enable_summary column to search_source_connectors -// v7: fixed connector-popup using invalid category for useInbox -const SYNC_VERSION = 7; - -// Database name prefix for identifying SurfSense databases -const DB_PREFIX = "surfsense-"; - -// Get Electric URL from environment -function getElectricUrl(): string { - if (typeof window !== "undefined") { - return process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133"; - } - return "http://localhost:5133"; -} - -/** - * Get the database name for a specific user - */ -function getDbName(userId: string): string { - return `idb://${DB_PREFIX}${userId}-v${SYNC_VERSION}`; -} - -/** - * Clean up databases from OTHER users AND old versions - * This is called on login to ensure clean state - */ -async function cleanupOtherUserDatabases(currentUserId: string): Promise { - if (typeof window === "undefined" || !window.indexedDB) { - return; - } - - // The exact database identifier we want to keep (current user + current version) - // Format: "surfsense-{userId}-v{version}" - const currentDbIdentifier = `${DB_PREFIX}${currentUserId}-v${SYNC_VERSION}`; - - try { - // Try to list all databases (not supported in all browsers) - if (typeof window.indexedDB.databases === "function") { - const databases = await window.indexedDB.databases(); - - for (const dbInfo of databases) { - const dbName = dbInfo.name; - if (!dbName) continue; - - // Check if this is a SurfSense database - if (dbName.includes("surfsense")) { - // Check if this is the current database - // PGlite stores with "/pglite/" prefix, so we check if the name ENDS WITH our identifier - if (dbName.endsWith(currentDbIdentifier)) { - debugLog(`[Electric] Keeping current database: ${dbName}`); - continue; - } - - // Delete ALL other databases (other users OR old versions of current user) - try { - debugLog(`[Electric] Deleting stale database: ${dbName}`); - window.indexedDB.deleteDatabase(dbName); - } catch (deleteErr) { - debugWarn(`[Electric] Failed to delete database ${dbName}:`, deleteErr); - } - } - } - } - } catch (err) { - // indexedDB.databases() not supported - that's okay, login cleanup is best-effort - debugWarn("[Electric] Could not enumerate databases for cleanup:", err); - } -} - -/** - * Initialize the Electric SQL client for a specific user - * - * KEY BEHAVIORS: - * 1. If already initialized for the SAME user, returns existing client - * 2. If initialized for a DIFFERENT user, closes old client and creates new one - * 3. On first init, cleans up databases from other users - * - * @param userId - The current user's ID (required) - */ -export async function initElectric(userId: string): Promise { - if (!userId) { - throw new Error("userId is required for Electric initialization"); - } - - // If already initialized for this user, return existing client - if (electricClient && currentUserId === userId) { - return electricClient; - } - - // If initialized for a different user, close the old client first - if (electricClient && currentUserId !== userId) { - debugLog(`[Electric] User changed from ${currentUserId} to ${userId}, reinitializing...`); - await cleanupElectric(); - } - - // If already initializing, wait for it - if (isInitializing && initPromise) { - return initPromise; - } - - isInitializing = true; - currentUserId = userId; - - initPromise = (async () => { - try { - // STEP 1: Clean up databases from other users (login-time cleanup) - debugLog("[Electric] Cleaning up databases from other users..."); - await cleanupOtherUserDatabases(userId); - - // STEP 2: Create user-specific PGlite database - const dbName = getDbName(userId); - debugLog(`[Electric] Initializing database: ${dbName}`); - - const db = await PGlite.create({ - dataDir: dbName, - relaxedDurability: true, - extensions: { - // Enable debug mode in electricSync only in development - electric: electricSync({ debug: process.env.NODE_ENV === "development" }), - live, // Enable live queries for real-time updates - }, - }); - - // STEP 3: Create the notifications table schema in PGlite - // This matches the backend schema - await db.exec(` - CREATE TABLE IF NOT EXISTS notifications ( - id INTEGER PRIMARY KEY, - user_id TEXT NOT NULL, - search_space_id INTEGER, - type TEXT NOT NULL, - title TEXT NOT NULL, - message TEXT NOT NULL, - read BOOLEAN NOT NULL DEFAULT FALSE, - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ - ); - - CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id); - CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read); - `); - - // Create the search_source_connectors table schema in PGlite - // This matches the backend schema - await db.exec(` - CREATE TABLE IF NOT EXISTS search_source_connectors ( - id INTEGER PRIMARY KEY, - search_space_id INTEGER NOT NULL, - user_id TEXT NOT NULL, - connector_type TEXT NOT NULL, - name TEXT NOT NULL, - is_indexable BOOLEAN NOT NULL DEFAULT FALSE, - last_indexed_at TIMESTAMPTZ, - config JSONB DEFAULT '{}', - periodic_indexing_enabled BOOLEAN NOT NULL DEFAULT FALSE, - indexing_frequency_minutes INTEGER, - next_scheduled_at TIMESTAMPTZ, - enable_summary BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - - CREATE INDEX IF NOT EXISTS idx_connectors_search_space_id ON search_source_connectors(search_space_id); - CREATE INDEX IF NOT EXISTS idx_connectors_type ON search_source_connectors(connector_type); - CREATE INDEX IF NOT EXISTS idx_connectors_user_id ON search_source_connectors(user_id); - `); - - // Create the documents table schema in PGlite - // Sync columns needed for real-time table display (lightweight - no content/metadata) - await db.exec(` - CREATE TABLE IF NOT EXISTS documents ( - id INTEGER PRIMARY KEY, - search_space_id INTEGER NOT NULL, - document_type TEXT NOT NULL, - title TEXT NOT NULL DEFAULT '', - created_by_id TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - status JSONB DEFAULT '{"state": "ready"}'::jsonb - ); - - CREATE INDEX IF NOT EXISTS idx_documents_search_space_id ON documents(search_space_id); - CREATE INDEX IF NOT EXISTS idx_documents_type ON documents(document_type); - CREATE INDEX IF NOT EXISTS idx_documents_search_space_type ON documents(search_space_id, document_type); - CREATE INDEX IF NOT EXISTS idx_documents_status ON documents((status->>'state')); - `); - - await db.exec(` - CREATE TABLE IF NOT EXISTS chat_comment_mentions ( - id INTEGER PRIMARY KEY, - comment_id INTEGER NOT NULL, - mentioned_user_id TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - - CREATE INDEX IF NOT EXISTS idx_chat_comment_mentions_user_id ON chat_comment_mentions(mentioned_user_id); - CREATE INDEX IF NOT EXISTS idx_chat_comment_mentions_comment_id ON chat_comment_mentions(comment_id); - `); - - // Create chat_comments table for live comment sync - await db.exec(` - CREATE TABLE IF NOT EXISTS chat_comments ( - id INTEGER PRIMARY KEY, - message_id INTEGER NOT NULL, - thread_id INTEGER NOT NULL, - parent_id INTEGER, - author_id TEXT, - content TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - - CREATE INDEX IF NOT EXISTS idx_chat_comments_thread_id ON chat_comments(thread_id); - CREATE INDEX IF NOT EXISTS idx_chat_comments_message_id ON chat_comments(message_id); - CREATE INDEX IF NOT EXISTS idx_chat_comments_parent_id ON chat_comments(parent_id); - `); - - // Create new_chat_messages table for live message sync - await db.exec(` - CREATE TABLE IF NOT EXISTS new_chat_messages ( - id INTEGER PRIMARY KEY, - thread_id INTEGER NOT NULL, - role TEXT NOT NULL, - content JSONB NOT NULL, - author_id TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - - CREATE INDEX IF NOT EXISTS idx_new_chat_messages_thread_id ON new_chat_messages(thread_id); - CREATE INDEX IF NOT EXISTS idx_new_chat_messages_created_at ON new_chat_messages(created_at); - `); - - const electricUrl = getElectricUrl(); - - // STEP 4: Create the client wrapper - electricClient = { - db, - userId, - syncShape: async (options: SyncShapeOptions): Promise => { - const { table, where, columns, primaryKey = ["id"] } = options; - - // Create cache key for this sync shape - const cacheKey = `${table}_${where || "all"}_${columns?.join(",") || "all"}`; - - // Check if we already have an active sync for this shape (memory optimization) - const existingHandle = activeSyncHandles.get(cacheKey); - if (existingHandle) { - debugLog(`[Electric] Reusing existing sync handle for: ${cacheKey}`); - return existingHandle; - } - - // Check if there's already a pending sync for this shape (prevent race condition) - const pendingSync = pendingSyncs.get(cacheKey); - if (pendingSync) { - debugLog(`[Electric] Waiting for pending sync to complete: ${cacheKey}`); - return pendingSync; - } - - // Create and track the sync promise to prevent race conditions - const syncPromise = (async (): Promise => { - // Build params for the shape request - // Electric SQL expects params as URL query parameters - const params: Record = { table }; - - // Validate and fix WHERE clause to ensure string literals are properly quoted - let validatedWhere = where; - if (where) { - // Check if where uses positional parameters - if (where.includes("$1")) { - // Extract the value from the where clause if it's embedded - // For now, we'll use the where clause as-is and let Electric handle it - params.where = where; - validatedWhere = where; - } else { - // Validate that string literals are properly quoted - // Count single quotes - should be even (pairs) for properly quoted strings - const singleQuoteCount = (where.match(/'/g) || []).length; - - if (singleQuoteCount % 2 !== 0) { - // Odd number of quotes means unterminated string literal - debugWarn("Where clause has unmatched quotes, fixing:", where); - // Add closing quote at the end - validatedWhere = `${where}'`; - params.where = validatedWhere; - } else { - // Use the where clause directly (already formatted) - params.where = where; - validatedWhere = where; - } - } - } - - if (columns) params.columns = columns.join(","); - - debugLog("[Electric] Syncing shape with params:", params); - debugLog("[Electric] Electric URL:", `${electricUrl}/v1/shape`); - debugLog("[Electric] Where clause:", where, "Validated:", validatedWhere); - - try { - // Debug: Test Electric SQL connection directly first (DEV ONLY - skipped in production) - if (process.env.NODE_ENV === "development") { - const testUrl = `${electricUrl}/v1/shape?table=${table}&offset=-1${validatedWhere ? `&where=${encodeURIComponent(validatedWhere)}` : ""}`; - debugLog("[Electric] Testing Electric SQL directly:", testUrl); - try { - const testResponse = await fetch(testUrl); - const testHeaders = { - handle: testResponse.headers.get("electric-handle"), - offset: testResponse.headers.get("electric-offset"), - upToDate: testResponse.headers.get("electric-up-to-date"), - }; - debugLog("[Electric] Direct Electric SQL response headers:", testHeaders); - const testData = await testResponse.json(); - debugLog( - "[Electric] Direct Electric SQL data count:", - Array.isArray(testData) ? testData.length : "not array", - testData - ); - } catch (testErr) { - console.error("[Electric] Direct Electric SQL test failed:", testErr); - } - } - - // Use PGlite's electric sync plugin to sync the shape - // According to Electric SQL docs, the shape config uses params for table, where, columns - // Note: mapColumns is OPTIONAL per pglite-sync types.ts - - // Create a promise that resolves when initial sync is complete - // Using recommended approach: check isUpToDate immediately, watch stream, shorter timeout - // IMPORTANT: We don't unsubscribe from the stream - it must stay active for real-time updates - let syncResolved = false; - // Initialize with no-op functions to satisfy TypeScript - let resolveInitialSync: () => void = () => {}; - let rejectInitialSync: (error: Error) => void = () => {}; - - const initialSyncPromise = new Promise((resolve, reject) => { - resolveInitialSync = () => { - if (!syncResolved) { - syncResolved = true; - // DON'T unsubscribe from stream - it needs to stay active for real-time updates - resolve(); - } - }; - rejectInitialSync = (error: Error) => { - if (!syncResolved) { - syncResolved = true; - // DON'T unsubscribe from stream even on error - let Electric handle it - reject(error); - } - }; - - // Shorter timeout (5 seconds) as fallback - setTimeout(() => { - if (!syncResolved) { - debugWarn( - `[Electric] ⚠️ Sync timeout for ${table} - checking isUpToDate one more time...` - ); - // Check isUpToDate one more time before resolving - // This will be checked after shape is created - setTimeout(() => { - if (!syncResolved) { - debugWarn( - `[Electric] ⚠️ Sync timeout for ${table} - resolving anyway after 5s` - ); - resolveInitialSync(); - } - }, 100); - } - }, 5000); - }); - - // ROOT CAUSE FIX: The duplicate key errors were caused by unstable cutoff dates - // in use-inbox.ts generating different sync keys on each render. - // That's now fixed (rounded to midnight UTC in getSyncCutoffDate). - // We can safely use shapeKey for fast incremental sync. - - const shapeKey = `${userId}_v${SYNC_VERSION}_${table}_${where?.replace(/[^a-zA-Z0-9]/g, "_") || "all"}`; - - // Type assertion to PGlite with electric extension - const pgWithElectric = db as unknown as { - electric: { - syncShapeToTable: ( - config: Record - ) => Promise<{ unsubscribe: () => void; isUpToDate: boolean; stream: unknown }>; - }; - }; - - const shapeConfig = { - shape: { - url: `${electricUrl}/v1/shape`, - params: { - table, - ...(validatedWhere ? { where: validatedWhere } : {}), - ...(columns ? { columns: columns.join(",") } : {}), - }, - }, - table, - primaryKey, - shapeKey, // Re-enabled for fast incremental sync (root cause in use-inbox.ts is fixed) - onInitialSync: () => { - debugLog( - `[Electric] ✅ Initial sync complete for ${table} - data should now be in PGlite` - ); - resolveInitialSync(); - }, - onError: (error: Error) => { - console.error(`[Electric] ❌ Shape sync error for ${table}:`, error); - console.error( - "[Electric] Error details:", - JSON.stringify(error, Object.getOwnPropertyNames(error)) - ); - rejectInitialSync(error); - }, - // Handle must-refetch: clear table data before Electric re-inserts from scratch - // This prevents "duplicate key" errors when the shape is invalidated - onMustRefetch: async (tx: Transaction) => { - debugLog( - `[Electric] ⚠️ Must refetch triggered for ${table} - clearing existing data` - ); - try { - // Delete rows matching the shape's WHERE clause - // If no WHERE clause, delete all rows from the table - if (validatedWhere) { - // Parse the WHERE clause to build a DELETE statement - // The WHERE clause is already validated and formatted - await tx.exec(`DELETE FROM ${table} WHERE ${validatedWhere}`); - debugLog(`[Electric] 🗑️ Cleared ${table} rows matching: ${validatedWhere}`); - } else { - // No WHERE clause means we're syncing the entire table - await tx.exec(`DELETE FROM ${table}`); - debugLog(`[Electric] 🗑️ Cleared all rows from ${table}`); - } - } catch (cleanupError) { - console.error( - `[Electric] ❌ Failed to clear ${table} during must-refetch:`, - cleanupError - ); - // Re-throw to let Electric handle the error - throw cleanupError; - } - }, - }; - - debugLog("[Electric] syncShapeToTable config:", JSON.stringify(shapeConfig, null, 2)); - - let shape: { unsubscribe: () => void; isUpToDate: boolean; stream: unknown }; - try { - shape = await pgWithElectric.electric.syncShapeToTable(shapeConfig); - } catch (syncError) { - // Handle "Already syncing" error - pglite-sync might not have fully cleaned up yet - const errorMessage = - syncError instanceof Error ? syncError.message : String(syncError); - if (errorMessage.includes("Already syncing")) { - debugWarn( - `[Electric] Already syncing ${table}, waiting for existing sync to settle...` - ); - - // Wait a short time for pglite-sync to settle - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Check if an active handle now exists (another sync might have completed) - const existingHandle = activeSyncHandles.get(cacheKey); - if (existingHandle) { - debugLog(`[Electric] Found existing handle after waiting: ${cacheKey}`); - return existingHandle; - } - - // Retry once after waiting - debugLog(`[Electric] Retrying sync for ${table}...`); - try { - shape = await pgWithElectric.electric.syncShapeToTable(shapeConfig); - } catch (retryError) { - const retryMessage = - retryError instanceof Error ? retryError.message : String(retryError); - if (retryMessage.includes("Already syncing")) { - // Still syncing - create a placeholder handle that indicates the table is being synced - debugWarn(`[Electric] ${table} still syncing, creating placeholder handle`); - const placeholderHandle: SyncHandle = { - unsubscribe: () => { - debugLog(`[Electric] Placeholder unsubscribe for: ${cacheKey}`); - activeSyncHandles.delete(cacheKey); - }, - get isUpToDate() { - return false; // We don't know the real state - }, - stream: undefined, - initialSyncPromise: Promise.resolve(), // Already syncing means data should be coming - }; - activeSyncHandles.set(cacheKey, placeholderHandle); - return placeholderHandle; - } - throw retryError; - } - } else { - throw syncError; - } - } - - if (!shape) { - throw new Error("syncShapeToTable returned undefined"); - } - - // Log the actual shape result structure - debugLog("[Electric] Shape sync result (initial):", { - hasUnsubscribe: typeof shape?.unsubscribe === "function", - isUpToDate: shape?.isUpToDate, - hasStream: !!shape?.stream, - streamType: typeof shape?.stream, - }); - - // Recommended Approach Step 1: Check isUpToDate immediately - if (shape.isUpToDate) { - debugLog( - `[Electric] ✅ Sync already up-to-date for ${table} (resuming from previous state)` - ); - resolveInitialSync(); - } else { - // Recommended Approach Step 2: Subscribe to stream and watch for "up-to-date" message - if (shape?.stream) { - const stream = shape.stream as any; - debugLog("[Electric] Shape stream details:", { - shapeHandle: stream?.shapeHandle, - lastOffset: stream?.lastOffset, - isUpToDate: stream?.isUpToDate, - error: stream?.error, - hasSubscribe: typeof stream?.subscribe === "function", - hasUnsubscribe: typeof stream?.unsubscribe === "function", - }); - - // Subscribe to the stream to watch for "up-to-date" control message - // NOTE: We keep this subscription active - don't unsubscribe! - // The stream is what Electric SQL uses for real-time updates - if (typeof stream?.subscribe === "function") { - debugLog( - "[Electric] Subscribing to shape stream to watch for up-to-date message..." - ); - // Subscribe but don't store unsubscribe - we want it to stay active - stream.subscribe((messages: unknown[]) => { - // Continue receiving updates even after sync is resolved - if (!syncResolved) { - debugLog( - "[Electric] 🔵 Shape stream received messages:", - messages?.length || 0 - ); - } - - // Check if any message indicates sync is complete - if (messages && messages.length > 0) { - for (const message of messages) { - const msg = message as any; - // Check for "up-to-date" control message - if ( - msg?.headers?.control === "up-to-date" || - msg?.headers?.electric_up_to_date === "true" || - (typeof msg === "object" && "up-to-date" in msg) - ) { - if (!syncResolved) { - debugLog(`[Electric] ✅ Received up-to-date message for ${table}`); - resolveInitialSync(); - } - // Continue listening for real-time updates - don't return! - } - } - if (!syncResolved && messages.length > 0) { - debugLog( - "[Electric] First message:", - JSON.stringify(messages[0], null, 2) - ); - } - } - - // Also check stream's isUpToDate property after receiving messages - if (!syncResolved && stream?.isUpToDate) { - debugLog(`[Electric] ✅ Stream isUpToDate is true for ${table}`); - resolveInitialSync(); - } - }); - - // Also check stream's isUpToDate property immediately - if (stream?.isUpToDate) { - debugLog(`[Electric] ✅ Stream isUpToDate is true immediately for ${table}`); - resolveInitialSync(); - } - } - - // Also poll isUpToDate periodically as a backup (every 200ms) - const pollInterval = setInterval(() => { - if (syncResolved) { - clearInterval(pollInterval); - return; - } - - if (shape.isUpToDate || stream?.isUpToDate) { - debugLog(`[Electric] ✅ Sync completed (detected via polling) for ${table}`); - clearInterval(pollInterval); - resolveInitialSync(); - } - }, 200); - - // Clean up polling when promise resolves - initialSyncPromise.finally(() => { - clearInterval(pollInterval); - }); - } else { - debugWarn( - `[Electric] ⚠️ No stream available for ${table}, relying on callback and timeout` - ); - } - } - - // Create the sync handle with proper cleanup - const syncHandle: SyncHandle = { - unsubscribe: () => { - debugLog(`[Electric] Unsubscribing from: ${cacheKey}`); - // Remove from cache first - activeSyncHandles.delete(cacheKey); - // Then unsubscribe from the shape - if (shape && typeof shape.unsubscribe === "function") { - shape.unsubscribe(); - } - }, - // Use getter to always return current state - get isUpToDate() { - return shape?.isUpToDate ?? false; - }, - stream: shape?.stream, - initialSyncPromise, // Expose promise so callers can wait for sync - }; - - // Cache the sync handle for reuse (memory optimization) - activeSyncHandles.set(cacheKey, syncHandle); - debugLog( - `[Electric] Cached sync handle for: ${cacheKey} (total cached: ${activeSyncHandles.size})` - ); - - return syncHandle; - } catch (error) { - console.error("[Electric] Failed to sync shape:", error); - // Check if Electric SQL server is reachable - try { - const response = await fetch(`${electricUrl}/v1/shape?table=${table}&offset=-1`, { - method: "GET", - }); - debugLog( - "[Electric] Electric SQL server response:", - response.status, - response.statusText - ); - if (!response.ok) { - console.error("[Electric] Electric SQL server error:", await response.text()); - } - } catch (fetchError) { - console.error("[Electric] Cannot reach Electric SQL server:", fetchError); - console.error("[Electric] Make sure Electric SQL is running at:", electricUrl); - } - throw error; - } - })(); - - // Track the sync promise to prevent concurrent syncs for the same shape - pendingSyncs.set(cacheKey, syncPromise); - - // Clean up the pending sync when done (whether success or failure) - syncPromise.finally(() => { - pendingSyncs.delete(cacheKey); - debugLog(`[Electric] Pending sync removed for: ${cacheKey}`); - }); - - return syncPromise; - }, - }; - - debugLog(`[Electric] ✅ Initialized successfully for user: ${userId}`); - return electricClient; - } catch (error) { - console.error("[Electric] Failed to initialize:", error); - // Reset state on failure - electricClient = null; - currentUserId = null; - throw error; - } finally { - isInitializing = false; - } - })(); - - return initPromise; -} - -/** - * Cleanup Electric SQL - close database and reset singleton - * Called on logout (best-effort) and when switching users - */ -export async function cleanupElectric(): Promise { - if (!electricClient) { - return; - } - - const userIdToClean = currentUserId; - debugLog(`[Electric] Cleaning up for user: ${userIdToClean}`); - - // Unsubscribe from all active sync handles first (memory cleanup) - debugLog(`[Electric] Unsubscribing from ${activeSyncHandles.size} active sync handles`); - // Copy keys to array to avoid mutation during iteration - const handleKeys = Array.from(activeSyncHandles.keys()); - for (const key of handleKeys) { - const handle = activeSyncHandles.get(key); - if (handle) { - try { - handle.unsubscribe(); - } catch (err) { - debugWarn(`[Electric] Failed to unsubscribe from ${key}:`, err); - } - } - } - // Ensure caches are empty - activeSyncHandles.clear(); - pendingSyncs.clear(); - - try { - // Close the PGlite database connection - await electricClient.db.close(); - debugLog("[Electric] Database closed"); - } catch (error) { - console.error("[Electric] Error closing database:", error); - } - - // Reset singleton state - electricClient = null; - currentUserId = null; - isInitializing = false; - initPromise = null; - - // Delete the user's IndexedDB database (best-effort cleanup on logout) - if (typeof window !== "undefined" && window.indexedDB && userIdToClean) { - try { - const dbName = `${DB_PREFIX}${userIdToClean}-v${SYNC_VERSION}`; - window.indexedDB.deleteDatabase(dbName); - debugLog(`[Electric] Deleted database: ${dbName}`); - } catch (err) { - debugWarn("[Electric] Failed to delete database:", err); - } - } - - debugLog("[Electric] Cleanup complete"); -} - -/** - * Get the Electric client (throws if not initialized) - */ -export function getElectric(): ElectricClient { - if (!electricClient) { - throw new Error("Electric not initialized. Call initElectric(userId) first."); - } - return electricClient; -} - -/** - * Check if Electric is initialized for a specific user - */ -export function isElectricInitialized(userId?: string): boolean { - if (!electricClient) return false; - if (userId && currentUserId !== userId) return false; - return true; -} - -/** - * Get the current user ID that Electric is initialized for - */ -export function getCurrentElectricUserId(): string | null { - return currentUserId; -} - -/** - * Get the PGlite database instance - */ -export function getDb(): PGlite | null { - return electricClient?.db ?? null; -} diff --git a/surfsense_web/lib/electric/context.ts b/surfsense_web/lib/electric/context.ts deleted file mode 100644 index 777d4e12c..000000000 --- a/surfsense_web/lib/electric/context.ts +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { createContext, useContext } from "react"; -import type { ElectricClient } from "./client"; - -/** - * Context for sharing the Electric SQL client across the app - * - * This ensures: - * 1. Single initialization point (ElectricProvider only) - * 2. No race conditions (hooks wait for context) - * 3. Clean cleanup (ElectricProvider manages lifecycle) - */ -export const ElectricContext = createContext(null); - -/** - * Hook to get the Electric client from context - * Returns null if Electric is not initialized yet - */ -export function useElectricClient(): ElectricClient | null { - return useContext(ElectricContext); -} - -/** - * Hook to get the Electric client, throwing if not available - * Use this when you're sure Electric should be initialized - */ -export function useElectricClientOrThrow(): ElectricClient { - const client = useContext(ElectricContext); - if (!client) { - throw new Error( - "Electric client not available. Make sure you're inside ElectricProvider and user is authenticated." - ); - } - return client; -} diff --git a/surfsense_web/lib/remotion/compile-check.ts b/surfsense_web/lib/remotion/compile-check.ts index 192d6f48e..de04c153d 100644 --- a/surfsense_web/lib/remotion/compile-check.ts +++ b/surfsense_web/lib/remotion/compile-check.ts @@ -2,12 +2,12 @@ import * as Babel from "@babel/standalone"; import React from "react"; import { AbsoluteFill, - useCurrentFrame, - useVideoConfig, - spring, + Easing, interpolate, Sequence, - Easing, + spring, + useCurrentFrame, + useVideoConfig, } from "remotion"; import { DURATION_IN_FRAMES } from "./constants"; @@ -21,7 +21,7 @@ function createStagger(totalFrames: number) { frame: number, fps: number, index: number, - total: number, + total: number ): { opacity: number; transform: string } { const enterPhase = Math.floor(totalFrames * 0.2); const exitStart = Math.floor(totalFrames * 0.8); @@ -43,9 +43,7 @@ function createStagger(totalFrames: number) { const opacity = s * (1 - exit); const translateY = - interpolate(s, [0, 1], [40, 0]) + - interpolate(exit, [0, 1], [0, -30]) + - ambient; + interpolate(s, [0, 1], [40, 0]) + interpolate(exit, [0, 1], [0, -30]) + ambient; const scale = interpolate(s, [0, 1], [0.97, 1]); return { @@ -97,7 +95,7 @@ export function prepareSource(code: string): string { const codeWithoutImports = code.replace(/^import\s+.*$/gm, "").trim(); const match = codeWithoutImports.match( - /export\s+(?:const|function)\s+(\w+)\s*(?::\s*React\.FC\s*)?=?\s*\(\s*\)\s*=>\s*\{([\s\S]*)\};?\s*$/, + /export\s+(?:const|function)\s+(\w+)\s*(?::\s*React\.FC\s*)?=?\s*\(\s*\)\s*=>\s*\{([\s\S]*)\};?\s*$/ ); if (match) { @@ -137,18 +135,10 @@ export function compileCheck(code: string): CompileResult { } } -export function compileToComponent( - code: string, - durationInFrames?: number, -): React.ComponentType { - const staggerFn = durationInFrames - ? createStagger(durationInFrames) - : defaultStagger; +export function compileToComponent(code: string, durationInFrames?: number): React.ComponentType { + const staggerFn = durationInFrames ? createStagger(durationInFrames) : defaultStagger; const jsCode = transpile(code); - const factory = new Function( - ...INJECTED_NAMES, - `${jsCode}\nreturn DynamicComponent;`, - ); + const factory = new Function(...INJECTED_NAMES, `${jsCode}\nreturn DynamicComponent;`); return factory(...buildInjectedValues(staggerFn)) as React.ComponentType; } diff --git a/surfsense_web/lib/remotion/dom-to-pptx.d.ts b/surfsense_web/lib/remotion/dom-to-pptx.d.ts index e832eb495..b451c7a33 100644 --- a/surfsense_web/lib/remotion/dom-to-pptx.d.ts +++ b/surfsense_web/lib/remotion/dom-to-pptx.d.ts @@ -13,6 +13,6 @@ declare module "dom-to-pptx" { export function exportToPptx( elementOrSelector: string | HTMLElement | Array, - options?: ExportOptions, + options?: ExportOptions ): Promise; } diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 2a33c9ab2..4868ac864 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -27,10 +27,6 @@ "@assistant-ui/react-ai-sdk": "^1.1.20", "@assistant-ui/react-markdown": "^0.11.9", "@babel/standalone": "^7.29.2", - "@electric-sql/client": "^1.4.0", - "@electric-sql/pglite": "^0.3.14", - "@electric-sql/pglite-sync": "^0.4.0", - "@electric-sql/react": "^1.0.26", "@hookform/resolvers": "^5.2.2", "@number-flow/react": "^0.5.10", "@platejs/autoformat": "^52.0.11", @@ -76,6 +72,7 @@ "@remotion/media": "^4.0.438", "@remotion/player": "^4.0.438", "@remotion/web-renderer": "^4.0.438", + "@rocicorp/zero": "^0.26.2", "@slate-serializers/html": "^2.2.3", "@streamdown/code": "^1.0.2", "@streamdown/math": "^1.0.2", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index 51c3f3f1e..0254eb9eb 100644 --- a/surfsense_web/pnpm-lock.yaml +++ b/surfsense_web/pnpm-lock.yaml @@ -26,18 +26,6 @@ importers: '@babel/standalone': specifier: ^7.29.2 version: 7.29.2 - '@electric-sql/client': - specifier: ^1.4.0 - version: 1.5.7 - '@electric-sql/pglite': - specifier: ^0.3.14 - version: 0.3.15 - '@electric-sql/pglite-sync': - specifier: ^0.4.0 - version: 0.4.1(@electric-sql/pglite@0.3.15) - '@electric-sql/react': - specifier: ^1.0.26 - version: 1.0.36(react@19.2.4) '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.71.2(react@19.2.4)) @@ -173,6 +161,9 @@ importers: '@remotion/web-renderer': specifier: ^4.0.438 version: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rocicorp/zero': + specifier: ^0.26.2 + version: 0.26.2(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)) '@slate-serializers/html': specifier: ^2.2.3 version: 2.2.3 @@ -1114,6 +1105,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@badrap/valita@0.3.11': + resolution: {integrity: sha512-oak0W8bycFjnrLeVCVvZqkOWTGh74wCPKUxGLJyhRukRs+V/hQdfZp1eDcQE4Gf3UrtJWfR/Ou4Xe0DZqJZ2FA==} + engines: {node: '>= 16'} + '@biomejs/biome@2.4.6': resolution: {integrity: sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==} engines: {node: '>=14.21.3'} @@ -1167,36 +1162,37 @@ packages: cpu: [x64] os: [win32] + '@databases/escape-identifier@1.0.3': + resolution: {integrity: sha512-Su36iSVzaHxpVdISVMViUX/32sLvzxVgjZpYhzhotxZUuLo11GVWsiHwqkvUZijTLUxcDmUqEwGJO3O/soLuZA==} + + '@databases/sql@3.3.0': + resolution: {integrity: sha512-vj9huEy4mjJ48GS1Z8yvtMm4BYAnFYACUds25ym6Gd/gsnngkJ17fo62a6mmbNNwCBS/8467PmZR01Zs/06TjA==} + + '@databases/validate-unicode@1.0.0': + resolution: {integrity: sha512-dLKqxGcymeVwEb/6c44KjOnzaAafFf0Wxa8xcfEjx/qOl3rdijsKYBAtIGhtVtOlpPf/PFKfgTuFurSPn/3B/g==} + '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@dotenvx/dotenvx@1.57.2': + resolution: {integrity: sha512-lv9+UZPnl/KOvShepevLWm3+/wc1It5kgO5Q580evnvOFMZcgKVEYFwxlL7Ohl9my1yjTsWo28N3PJYUEO8wFQ==} + hasBin: true + + '@drdgvhbh/postgres-error-codes@0.0.6': + resolution: {integrity: sha512-tAz0Xp+qhq90x0r/3VW96iRdHFw72cYQqXa65u0eFVhSMC27bc2gZ8Ky5WXEmshrl/bCe7QTYBNEF0U5zeSQjw==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@electric-sql/client@1.5.7': - resolution: {integrity: sha512-jeKLlooV/wIBVkZV6d3opT+eoDLds9qDzHtUYezfl6BzAPY1v5mApgjlG/evbvBBZKYW778wOIBUfq16SMnr7w==} - - '@electric-sql/experimental@1.0.14': - resolution: {integrity: sha512-Wpv9UC7r4JYnQO4GbtQve7SVIX/VcBwAP12Xx11ObK2TBI+NJHtDRimUEKKQorq9Kr6yTAJ3BYOKCINxFsbDIg==} + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} peerDependencies: - '@electric-sql/client': 1.0.14 - - '@electric-sql/pglite-sync@0.4.1': - resolution: {integrity: sha512-6BWUZbaLNRd4HG5tB4QGDzD4GlJ94Q+gfygotRxCo8hsYjjUowAlnKAB6QTtbEly4R9Q1xqCqCpifiVvWXaO5Q==} - peerDependencies: - '@electric-sql/pglite': 0.3.15 + '@noble/ciphers': ^1.0.0 '@electric-sql/pglite@0.3.15': resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} - '@electric-sql/react@1.0.36': - resolution: {integrity: sha512-vowuzMdyRtyI+ycUoqP9bqd0+hyYLHYZBC2sV2BGaGToQy+1vrZO1pPXDD8gTcajpd6lP9k08OTp8VP+yW4uOw==} - peerDependencies: - react: '>=18.3.1 <20.0.0' - peerDependenciesMeta: - react: - optional: true - '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -1696,6 +1692,30 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/cors@10.1.0': + resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/websocket@11.2.0': + resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==} + '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -1740,6 +1760,19 @@ packages: tailwindcss: optional: true + '@google-cloud/precise-date@4.0.0': + resolution: {integrity: sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==} + engines: {node: '>=14.0.0'} + + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -1914,6 +1947,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@juggle/resize-observer@3.4.0': resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} @@ -1935,9 +1971,6 @@ packages: peerDependencies: mediabunny: ^1.0.0 - '@microsoft/fetch-event-source@2.0.1': - resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} - '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1995,6 +2028,18 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2017,6 +2062,10 @@ packages: react: ^18 || ^19 react-dom: ^18 || ^19 + '@opentelemetry/api-logs@0.203.0': + resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.208.0': resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} @@ -2025,6 +2074,31 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/auto-instrumentations-node@0.62.2': + resolution: {integrity: sha512-Ipe6X7ddrCiRsuewyTU83IvKiSFT4piqmv9z8Ovg1E7v98pdTj1pUE6sDrHV50zl7/ypd+cONBgt+EYSZu4u9Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + '@opentelemetry/core': ^2.0.0 + + '@opentelemetry/context-async-hooks@2.0.1': + resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/context-async-hooks@2.6.0': + resolution: {integrity: sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.1': + resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.2.0': resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -2037,24 +2111,412 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.6.0': + resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.203.0': + resolution: {integrity: sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.203.0': + resolution: {integrity: sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-http@0.208.0': resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-proto@0.203.0': + resolution: {integrity: sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0': + resolution: {integrity: sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.203.0': + resolution: {integrity: sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.203.0': + resolution: {integrity: sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.203.0': + resolution: {integrity: sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.203.0': + resolution: {integrity: sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.203.0': + resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.203.0': + resolution: {integrity: sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.0.1': + resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.50.0': + resolution: {integrity: sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.54.1': + resolution: {integrity: sha512-qm8pGSAM1mXk7unbrGktWWGJc6IFI58ZsaHJ+i420Fp5VO3Vf7GglIgaXTS8CKBrVB4LHFj3NvzJg31PtsAQcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.58.0': + resolution: {integrity: sha512-9vFH7gU686dsAeLMCkqUj9y0MQZ1xrTtStSpNV2UaGWtDnRjJrAdJLu9Y545oKEaDTeVaob4UflyZvvpZnw3Xw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.49.0': + resolution: {integrity: sha512-ky5Am1y6s3Ex/3RygHxB/ZXNG07zPfg9Z6Ora+vfeKcr/+I6CJbWXWhSBJor3gFgKN3RvC11UWVURnmDpBS6Pg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.49.0': + resolution: {integrity: sha512-BNIvqldmLkeikfI5w5Rlm9vG5NnQexfPoxOgEMzfDVOEF+vS6351I6DzWLLgWWR9CNF/jQJJi/lr6am2DLp0Rw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.47.0': + resolution: {integrity: sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.19.0': + resolution: {integrity: sha512-99ms8kQWRuPt5lkDqbJJzD+7Tq5TMUlBZki4SA2h6CgK4ncX+tyep9XFY1e+XTBLJIWmuFMGbWqBLJ4fSKIQNQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.21.1': + resolution: {integrity: sha512-hNAm/bwGawLM8VDjKR0ZUDJ/D/qKR3s6lA5NV+btNaPVm2acqhPcT47l2uCVi+70lng2mywfQncor9v8/ykuyw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.47.0': + resolution: {integrity: sha512-775fOnewWkTF4iXMGKgwvOGqEmPrU1PZpXjjqvTrEErYBJe7Fz1WlEeUStHepyKOdld7Ghv7TOF/kE3QDctvrg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.52.0': + resolution: {integrity: sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.48.0': + resolution: {integrity: sha512-3zQlE/DoVfVH6/ycuTv7vtR/xib6WOa0aLFfslYcvE62z0htRu/ot8PV/zmMZfnzpTQj8S/4ULv36R6UIbpJIg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.23.0': + resolution: {integrity: sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.47.0': + resolution: {integrity: sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.51.0': + resolution: {integrity: sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.203.0': + resolution: {integrity: sha512-Qmjx2iwccHYRLoE4RFS46CvQE9JG9Pfeae4EPaNZjvIuJxb/pZa2R9VWzRlTehqQWpAvto/dGhtkw8Tv+o0LTg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.50.0': + resolution: {integrity: sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.203.0': + resolution: {integrity: sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.51.0': + resolution: {integrity: sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.13.0': + resolution: {integrity: sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.48.0': + resolution: {integrity: sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.51.0': + resolution: {integrity: sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.48.0': + resolution: {integrity: sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.47.0': + resolution: {integrity: sha512-vXDs/l4hlWy1IepPG1S6aYiIZn+tZDI24kAzwKKJmR2QEJRL84PojmALAEJGazIOLl/VdcCPZdMb0U2K0VzojA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.56.0': + resolution: {integrity: sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.50.0': + resolution: {integrity: sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.50.0': + resolution: {integrity: sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.49.0': + resolution: {integrity: sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.49.0': + resolution: {integrity: sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.47.0': + resolution: {integrity: sha512-csoJ++Njpf7C09JH+0HNGenuNbDZBqO1rFhMRo6s0rAmJwNh9zY3M/urzptmKlqbKnf4eH0s+CKHy/+M8fbFsQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-oracledb@0.29.0': + resolution: {integrity: sha512-2aHLiJdkyiUbooIUm7FaZf+O4jyqEl+RfFpgud1dxT87QeeYM216wi+xaMNzsb5yKtRBqbA3qeHBCyenYrOZwA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.56.1': + resolution: {integrity: sha512-0/PiHDPVaLdcXNw6Gqb3JBdMxComMEwh444X8glwiynJKJHRTR49+l2cqJfoOVzB8Sl1XRl3Yaqw6aDi3s8e9w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.50.1': + resolution: {integrity: sha512-pBbvuWiHA9iAumAuQ0SKYOXK7NRlbnVTf/qBV0nMdRnxBPrc/GZTbh0f7Y59gZfYsbCLhXLL1oRTEnS+PwS3CA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.52.0': + resolution: {integrity: sha512-R8Y7cCZlJ2Vl31S2i7bl5SqyC/aul54ski4wCFip/Tp9WGtLK1xVATi2rwy2wkc8ZCtjdEe9eEVR+QFG6gGZxg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.49.0': + resolution: {integrity: sha512-tsGZZhS4mVZH7omYxw5jpsrD3LhWizqWc0PYtAnzpFUvL5ZINHE+cm57bssTQ2AK/GtZMxu9LktwCvIIf3dSmw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.48.0': + resolution: {integrity: sha512-Wixrc8CchuJojXpaS/dCQjFOMc+3OEil1H21G+WLYQb8PcKt5kzW9zDBT19nyjjQOx/D/uHPfgbrT+Dc7cfJ9w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-runtime-node@0.17.1': + resolution: {integrity: sha512-c1FlAk+bB2uF9a8YneGmNPTl7c/xVaan4mmWvbkWcOmH/ipKqR1LaKUlz/BMzLrJLjho1EJlG2NrS2w2Arg+nw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.50.0': + resolution: {integrity: sha512-6JN6lnKN9ZuZtZdMQIR+no1qHzQvXSZUsNe3sSWMgqmNRyEXuDUWBIyKKeG0oHRHtR4xE4QhJyD4D5kKRPWZFA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.22.0': + resolution: {integrity: sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.14.0': + resolution: {integrity: sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.48.1': + resolution: {integrity: sha512-XyOuVwdziirHHYlsw+BWrvdI/ymjwnexupKA787zQQ+D5upaE/tseZxjfQa7+t4+FdVLxHICaMTmkSD4yZHpzQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.203.0': + resolution: {integrity: sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.203.0': + resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.208.0': resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.203.0': + resolution: {integrity: sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.203.0': + resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.208.0': resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/propagator-b3@2.0.1': + resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.0.1': + resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.38.2': + resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resource-detector-alibaba-cloud@0.31.11': + resolution: {integrity: sha512-R/asn6dAOWMfkLeEwqHCUz0cNbb9oiHVyd11iwlypeT/p9bR1lCX5juu5g/trOwxo62dbuFcDbBdKCJd3O2Edg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@2.13.0': + resolution: {integrity: sha512-ZPCn7gZhGqUYUoD+RCHIlayoHBMaJaEjfqlgz2EPKoXJ4y7Ru7CUm+Tm3yJVMKF92cN9xUQR0j5KALyF0fg9aw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.10.0': + resolution: {integrity: sha512-5cNAiyPBg53Uxe/CW7hsCq8HiKNAUGH+gi65TtgpzSR9bhJG4AEbuZhbJDFwe97tn2ifAD1JTkbc/OFuaaFWbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.7.11': + resolution: {integrity: sha512-XUxnGuANa/EdxagipWMXKYFC7KURwed9/V0+NtYjFmwWHzV9/J4IYVGTK8cWDpyUvAQf/vE4sMa3rnS025ivXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.37.0': + resolution: {integrity: sha512-LGpJBECIMsVKhiulb4nxUw++m1oF4EiDDPmFGW2aqYaAF0oUvJNv8Z/55CAzcZ7SxvlTgUwzewXDBsuCup7iqw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.2.0': resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} engines: {node: ^18.19.0 || >=20.6.0} @@ -2067,28 +2529,82 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.6.0': + resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.203.0': + resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.208.0': resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.2.0': resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-node@0.203.0': + resolution: {integrity: sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.2.0': resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.6.0': + resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.0.1': + resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.6.0': + resolution: {integrity: sha512-YhswtasmsbIGEFvLGvR9p/y3PVRTfFf+mgY8van4Ygpnv4sA3vooAjvh+qAn9PNWxs4/IwGGqiQS0PPsaRJ0vQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.39.0': resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} engines: {node: '>=14'} + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@orama/orama@3.1.18': resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} @@ -2175,6 +2691,9 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@platejs/autoformat@52.0.11': resolution: {integrity: sha512-5/pLa0uAOrN/jfwpEWEhk/5G9wmSD12EnBnVDP0lB2tuosOzyMCH0rNylMYwgLFACNO7goLw7ka6tvjJ1ErxtQ==} peerDependencies: @@ -2311,6 +2830,10 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' + '@postgresql-typed/oids@0.2.0': + resolution: {integrity: sha512-jh1nIP/nmtlZkj1t0cO2NC2lFHg/fXQhtRFsL70Rh/5ELp5fqxH/calwPVTkS8gPae1k/PTqQYbU23E+Q2q0rg==} + engines: {node: '>=16', pnpm: '>=8.6.0'} + '@posthog/core@1.23.1': resolution: {integrity: sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==} @@ -3393,6 +3916,35 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' + '@rocicorp/lock@1.0.4': + resolution: {integrity: sha512-FavTiO8ETXFXDVfA87IThGduTTTR8iqzBnr/c60gUUmbk7knGEXPmf2B+yiNuluJD0ku0fL2V2r62UXnsLXl6w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + '@rocicorp/logger@5.4.0': + resolution: {integrity: sha512-kmMR5iLrwRIsvPZ+UXnmyAM3Mlvz6rCHrYfMsrMPgFYKLfo7amUH1RwHo6tuuqJiAvUbeaCoDtc8e+V0Mr4PSA==} + + '@rocicorp/resolver@1.0.2': + resolution: {integrity: sha512-TfjMTQp9cNNqNtHFfa+XHEGdA7NnmDRu+ZJH4YF3dso0Xk/b9DMhg/sl+b6CR4ThFZArXXDsG1j8Mwl34wcOZQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + '@rocicorp/zero-sqlite3@1.0.15': + resolution: {integrity: sha512-o4ezzTgNQvOM23X/mBRN4LAo0i7XkIDAL3Obg7FaJLFTuwJyYwqE45aqk9vg8/0mlBw2U+DEUGLP1q/Pf+VgYQ==} + engines: {bun: '>=1.1.0', node: 20.x || 22.x || 23.x || 24.x} + hasBin: true + + '@rocicorp/zero@0.26.2': + resolution: {integrity: sha512-fq67gwxvV3rWx/QeKTXu6ab2CLqR7O9GrcZPbI5bCBxHgRB2S+VSxEvcbEtYDjyZvI3GWmisa5UdsYN3H2vYsA==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + '@op-engineering/op-sqlite': '>=15' + expo-sqlite: '>=15' + peerDependenciesMeta: + '@op-engineering/op-sqlite': + optional: true + expo-sqlite: + optional: true + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -3868,6 +4420,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/aws-lambda@8.10.152': + resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3883,9 +4438,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/basic-auth@1.1.8': + resolution: {integrity: sha512-dKcUeixGuZn8pBjcUrf1N7x5K6lWuKuwHHitM2IZ4vwZUDWEhhNtwCWiba8jTA9zn0GQQ+fTFkWpKx8pOU/enw==} + + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -3931,15 +4495,30 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} '@types/node@20.19.33': resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + '@types/oracledb@6.5.2': + resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} + + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.15.5': + resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + '@types/pg@8.16.0': resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} @@ -3954,6 +4533,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -3963,6 +4545,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.56.0': resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4151,6 +4736,14 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4161,6 +4754,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ai@4.3.19: resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} engines: {node: '>=18'} @@ -4177,9 +4774,32 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -4195,6 +4815,10 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + array-back@6.2.3: + resolution: {integrity: sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==} + engines: {node: '>=12.17'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -4247,6 +4871,10 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + attr-accept@2.2.5: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} @@ -4255,6 +4883,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axe-core@4.11.1: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} @@ -4292,11 +4923,27 @@ packages: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.0: resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} engines: {node: '>=6.0.0'} hasBin: true + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -4319,6 +4966,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -4348,6 +4998,14 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + + chalk-template@1.1.2: + resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==} + engines: {node: '>=14.16'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -4377,10 +5035,20 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -4390,6 +5058,14 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cloudevents@10.0.0: + resolution: {integrity: sha512-uyzC+PpMMRawbouHO+3mlisr3QfEDObmo2pN4oTTF6dZncZgpIzdasZx0tRBFI1dMsqCLZZXMtz8cUuvYqHdbw==} + engines: {node: '>=20 <=24'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -4422,6 +5098,23 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + command-line-args@6.0.2: + resolution: {integrity: sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==} + engines: {node: '>=12.20'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + + command-line-usage@7.0.4: + resolution: {integrity: sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==} + engines: {node: '>=12.20.0'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -4430,6 +5123,9 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + compare-utf8@0.1.1: + resolution: {integrity: sha512-bND8Irz+KrF96w4Tkm1m8u5q8iE2fnvP196sHGy7XNrGNXlhyl07VnsCRYrXgEhhf/lM7hyCKRnMeh8Icis4Sw==} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -4439,6 +5135,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -4546,6 +5246,14 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -4561,6 +5269,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -4715,6 +5426,13 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eciesjs@0.4.18: + resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + electron-to-chromium@1.5.302: resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} @@ -4724,9 +5442,15 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -4959,13 +5683,27 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4976,9 +5714,24 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.8.4: + resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -5005,10 +5758,26 @@ packages: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + + find-replace@5.0.2: + resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} + engines: {node: '>=14'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -5031,6 +5800,9 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + framer-motion@12.34.3: resolution: {integrity: sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==} peerDependencies: @@ -5045,6 +5817,9 @@ packages: react-dom: optional: true + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5164,6 +5939,14 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + geist@1.7.0: resolution: {integrity: sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==} peerDependencies: @@ -5177,6 +5960,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -5189,6 +5976,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -5196,6 +5987,9 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -5215,6 +6009,10 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -5332,12 +6130,23 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + https@1.0.0: resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + icu-minify@4.8.3: resolution: {integrity: sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -5366,6 +6175,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -5373,6 +6185,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -5383,6 +6198,10 @@ packages: intl-messageformat@11.1.2: resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -5395,6 +6214,10 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -5447,6 +6270,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -5464,6 +6291,10 @@ packages: is-hotkey@0.2.0: resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + is-in-subnet@4.0.1: + resolution: {integrity: sha512-D3mAuAo6vZ+/AxsLkEIZ3moTx7AIGQLLzLQslV6n0RRO/CzdUemXap+lj3OPAehKCbdkGPikxOVUYqRo0GGJAA==} + engines: {node: '>=10.23.0'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -5500,6 +6331,10 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -5533,6 +6368,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -5541,6 +6380,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jotai-optics@0.4.0: resolution: {integrity: sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g==} peerDependencies: @@ -5605,6 +6447,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-xxhash@4.0.0: + resolution: {integrity: sha512-3Q2eIqG6s1KEBBmkj9tGM9lef8LJbuRyTVBdI3GpTnrvtytunjLPO0wqABp5qhtMzfA32jYn1FlnIV7GH1RAHQ==} + engines: {node: '>=18.0.0'} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -5614,15 +6460,27 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-custom-numbers@3.1.1: + resolution: {integrity: sha512-rYIAIuiIRy58aax2tuZb7HawKFATBG848PiguybJh/R+pvC8jxjEOVBQHj4J3U2D4/Y4acBCO4A/glILW8wPoA==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -5650,6 +6508,9 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kasi@1.1.2: + resolution: {integrity: sha512-Q2N8EHdkJFKdzq8fxzDNSXb4RE8xzcPHwZuT7N1/wJY3XaMkoGXkzGZcEnNODzQXJNOd9inReEW2V1V4svAf/Q==} + katex@0.16.22: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true @@ -5689,6 +6550,9 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.31.1: resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} engines: {node: '>= 12.0.0'} @@ -5766,6 +6630,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -5894,6 +6761,9 @@ packages: mediabunny@1.39.2: resolution: {integrity: sha512-VcrisGRt+OI7tTPrziucJoCIPYIS/DEWY37TqzQVLWSUUHiyvsiRizEypQ3FOlhfIZ4ytAG/Mw4zxfetCTyKUg==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -6010,6 +6880,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.1.3: resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==} @@ -6020,6 +6898,15 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mnemonist@0.40.0: + resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + motion-dom@12.34.3: resolution: {integrity: sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==} @@ -6057,6 +6944,9 @@ packages: engines: {node: ^18 || >=20} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -6112,6 +7002,10 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-abi@3.89.0: + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} + engines: {node: '>=10'} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -6119,9 +7013,22 @@ packages: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + npm-to-yarn@3.0.1: resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6144,6 +7051,10 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -6164,6 +7075,20 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -6214,6 +7139,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-prometheus-text-format@1.1.1: + resolution: {integrity: sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -6244,6 +7172,10 @@ packages: pg-connection-string@2.11.0: resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-format-fix@1.0.5: + resolution: {integrity: sha512-HcXVy9Zk4kn87P0+U9XSxGtenNyknbPB87NreixSBk0lYJy89u+d/zQbS+f/aTTxABQ/B6FH1KdBB5EsGzRS2w==} + engines: {node: '>=4.0'} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -6283,6 +7215,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + platejs@52.0.17: resolution: {integrity: sha512-vJZijt8coKh6W60RmUG69DFM4ZwLhzNwSfOwfk5SkVWer1iDqqyW34JnGn5nKI8irj+45/sSzx4Nfgw9CR+QYw==} peerDependencies: @@ -6328,6 +7270,10 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + postgres@3.4.8: resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} engines: {node: '>=12'} @@ -6345,10 +7291,21 @@ packages: preact@10.28.4: resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + prismjs@1.27.0: resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} engines: {node: '>=6'} @@ -6360,6 +7317,16 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -6376,6 +7343,9 @@ packages: proxy-compare@2.6.0: resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6389,6 +7359,9 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + radix-ui@1.4.3: resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} peerDependencies: @@ -6405,6 +7378,10 @@ packages: raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-compiler-runtime@1.0.0: resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==} peerDependencies: @@ -6565,10 +7542,22 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} @@ -6669,6 +7658,18 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -6686,10 +7687,17 @@ packages: engines: {node: '>= 0.4'} hasBin: true + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -6713,6 +7721,14 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -6737,6 +7753,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6752,6 +7771,9 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6783,6 +7805,15 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + slate-dom@0.119.0: resolution: {integrity: sha512-foc8a2NkE+1SldDIYaoqjhVKupt8RSuvHI868rfYOcypD4we5TT7qunjRKJ852EIRh/Ql8sSTepXgXKOUJnt1w==} peerDependencies: @@ -6810,6 +7841,9 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -6848,12 +7882,19 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamdown@2.3.0: resolution: {integrity: sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} @@ -6886,10 +7927,22 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -6937,6 +7990,10 @@ packages: tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + table-layout@4.1.1: + resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} + engines: {node: '>=12.17'} + tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -6957,9 +8014,20 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + text-segmentation@1.0.3: resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -6985,9 +8053,16 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -7022,6 +8097,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -7047,6 +8125,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + typical@7.3.0: + resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} + engines: {node: '>=12.17'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -7112,6 +8194,13 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-pattern@1.0.3: + resolution: {integrity: sha512-uQcEj/2puA4aq1R3A2+VNVBgaWYR24FdWjl7VNW83rnWftlhyzOZ/tBjezRiC2UkIzuxC8Top3IekN3vUf1WxA==} + engines: {node: '>=0.12.0'} + + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -7195,9 +8284,20 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utrie@1.0.2: resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -7259,6 +8359,12 @@ packages: web-vitals@5.1.0: resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -7280,17 +8386,57 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -8197,6 +9343,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@badrap/valita@0.3.11': {} + '@biomejs/biome@2.4.6': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.4.6 @@ -8232,36 +9380,38 @@ snapshots: '@biomejs/cli-win32-x64@2.4.6': optional: true + '@databases/escape-identifier@1.0.3': + dependencies: + '@databases/validate-unicode': 1.0.0 + + '@databases/sql@3.3.0': {} + + '@databases/validate-unicode@1.0.0': {} + '@date-fns/tz@1.4.1': {} + '@dotenvx/dotenvx@1.57.2': + dependencies: + commander: 11.1.0 + dotenv: 17.3.1 + eciesjs: 0.4.18 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.3) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.3 + which: 4.0.0 + + '@drdgvhbh/postgres-error-codes@0.0.6': {} + '@drizzle-team/brocli@0.10.2': {} - '@electric-sql/client@1.5.7': + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': dependencies: - '@microsoft/fetch-event-source': 2.0.1 - optionalDependencies: - '@rollup/rollup-darwin-arm64': 4.59.0 + '@noble/ciphers': 1.3.0 - '@electric-sql/experimental@1.0.14(@electric-sql/client@1.5.7)': - dependencies: - '@electric-sql/client': 1.5.7 - optionalDependencies: - '@rollup/rollup-darwin-arm64': 4.59.0 - - '@electric-sql/pglite-sync@0.4.1(@electric-sql/pglite@0.3.15)': - dependencies: - '@electric-sql/client': 1.5.7 - '@electric-sql/experimental': 1.0.14(@electric-sql/client@1.5.7) - '@electric-sql/pglite': 0.3.15 - - '@electric-sql/pglite@0.3.15': {} - - '@electric-sql/react@1.0.36(react@19.2.4)': - dependencies: - '@electric-sql/client': 1.5.7 - use-sync-external-store: 1.6.0(react@19.2.4) - optionalDependencies: - react: 19.2.4 + '@electric-sql/pglite@0.3.15': + optional: true '@emnapi/core@1.8.1': dependencies: @@ -8557,6 +9707,43 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + + '@fastify/cors@10.1.0': + dependencies: + fastify-plugin: 5.1.0 + mnemonist: 0.40.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + + '@fastify/websocket@11.2.0': + dependencies: + duplexify: 4.1.3 + fastify-plugin: 5.1.0 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -8615,6 +9802,20 @@ snapshots: optionalDependencies: tailwindcss: 4.2.1 + '@google-cloud/precise-date@4.0.0': {} + + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@hookform/resolvers@5.2.2(react-hook-form@7.71.2(react@19.2.4))': dependencies: '@standard-schema/utils': 0.3.0 @@ -8747,6 +9948,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@juggle/resize-observer@3.4.0': {} '@mdx-js/mdx@3.1.1': @@ -8791,8 +9994,6 @@ snapshots: dependencies: mediabunny: 1.39.2 - '@microsoft/fetch-event-source@2.0.1': {} - '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -8830,6 +10031,14 @@ snapshots: '@next/swc-win32-x64-msvc@16.1.6': optional: true + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8851,12 +10060,85 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@opentelemetry/api-logs@0.203.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} + '@opentelemetry/auto-instrumentations-node@0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.54.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.58.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.19.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.21.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.13.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-oracledb': 0.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.56.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.50.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-runtime-node': 0.17.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.22.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.14.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.48.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.31.11(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 2.13.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.7.11(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.37.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8867,6 +10149,30 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8876,12 +10182,472 @@ snapshots: '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.54.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@types/aws-lambda': 8.10.152 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.58.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.19.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.21.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.13.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-oracledb@0.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@types/oracledb': 6.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.56.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + '@types/pg': 8.15.5 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.50.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-runtime-node@0.17.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.22.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.14.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.48.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8893,6 +10659,61 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 + '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/redis-common@0.38.2': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.31.11(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-aws@2.13.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/resource-detector-azure@0.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/resource-detector-container@0.7.11(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-gcp@0.37.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8905,6 +10726,19 @@ snapshots: '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8912,12 +10746,53 @@ snapshots: '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8925,8 +10800,34 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-node@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions@1.39.0': {} + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@orama/orama@3.1.18': {} '@parcel/watcher-android-arm64@2.5.6': @@ -8989,6 +10890,8 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 + '@pinojs/redact@0.4.0': {} + '@platejs/autoformat@52.0.11(platejs@52.0.17(@types/react@19.2.14)(immer@10.2.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.4)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: lodash: 4.17.23 @@ -9194,6 +11097,8 @@ snapshots: - scheduler - use-sync-external-store + '@postgresql-typed/oids@0.2.0': {} + '@posthog/core@1.23.1': dependencies: cross-spawn: 7.0.6 @@ -10331,6 +12236,79 @@ snapshots: react-dom: 19.2.4(react@19.2.4) remotion: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rocicorp/lock@1.0.4': + dependencies: + '@rocicorp/resolver': 1.0.2 + + '@rocicorp/logger@5.4.0': {} + + '@rocicorp/resolver@1.0.2': {} + + '@rocicorp/zero-sqlite3@1.0.15': + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + '@rocicorp/zero@0.26.2(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))': + dependencies: + '@badrap/valita': 0.3.11 + '@databases/escape-identifier': 1.0.3 + '@databases/sql': 3.3.0 + '@dotenvx/dotenvx': 1.57.2 + '@drdgvhbh/postgres-error-codes': 0.0.6 + '@fastify/cors': 10.1.0 + '@fastify/websocket': 11.2.0 + '@google-cloud/precise-date': 4.0.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/auto-instrumentations-node': 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.6.0(@opentelemetry/api@1.9.0) + '@postgresql-typed/oids': 0.2.0 + '@rocicorp/lock': 1.0.4 + '@rocicorp/logger': 5.4.0 + '@rocicorp/resolver': 1.0.2 + '@rocicorp/zero-sqlite3': 1.0.15 + '@standard-schema/spec': 1.1.0 + '@types/basic-auth': 1.1.8 + '@types/ws': 8.18.1 + basic-auth: 2.0.1 + chalk: 5.6.2 + chalk-template: 1.1.2 + chokidar: 4.0.3 + cloudevents: 10.0.0 + command-line-args: 6.0.2 + command-line-usage: 7.0.4 + compare-utf8: 0.1.1 + defu: 6.1.4 + eventemitter3: 5.0.4 + fastify: 5.8.4 + is-in-subnet: 4.0.1 + jose: 5.10.0 + js-xxhash: 4.0.0 + json-custom-numbers: 3.1.1 + kasi: 1.1.2 + nanoid: 5.1.6 + parse-prometheus-text-format: 1.1.1 + pg-format: pg-format-fix@1.0.5 + postgres: 3.4.7 + prettier: 3.8.1 + semver: 7.7.4 + tsx: 4.21.0 + url-pattern: 1.0.3 + urlpattern-polyfill: 10.1.0 + ws: 8.20.0 + transitivePeerDependencies: + - '@75lb/nature' + - '@opentelemetry/core' + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -10761,6 +12739,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aws-lambda@8.10.152': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -10791,8 +12771,20 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/basic-auth@1.1.8': + dependencies: + '@types/node': 20.19.33 + + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 20.19.33 + '@types/canvas-confetti@1.9.0': {} + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.33 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -10835,8 +12827,16 @@ snapshots: '@types/mdx@2.0.13': {} + '@types/memcached@2.2.10': + dependencies: + '@types/node': 20.19.33 + '@types/ms@2.1.0': {} + '@types/mysql@2.15.27': + dependencies: + '@types/node': 20.19.33 + '@types/node@18.19.130': dependencies: undici-types: 5.26.5 @@ -10845,6 +12845,20 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/oracledb@6.5.2': + dependencies: + '@types/node': 20.19.33 + + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.16.0 + + '@types/pg@8.15.5': + dependencies: + '@types/node': 20.19.33 + pg-protocol: 1.11.0 + pg-types: 2.2.0 + '@types/pg@8.16.0': dependencies: '@types/node': 20.19.33 @@ -10863,6 +12877,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/tedious@4.0.14': + dependencies: + '@types/node': 20.19.33 + '@types/trusted-types@2.0.7': optional: true @@ -10870,6 +12888,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.33 + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -11056,12 +13078,20 @@ snapshots: '@xmldom/xmldom@0.8.11': {} + abstract-logging@2.0.1: {} + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} + agent-base@7.1.4: {} + ai@4.3.19(react@19.2.4)(zod@4.3.6): dependencies: '@ai-sdk/provider': 1.1.3 @@ -11082,6 +13112,14 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -11089,6 +13127,15 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -11101,6 +13148,8 @@ snapshots: aria-query@5.3.2: {} + array-back@6.2.3: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -11190,12 +13239,19 @@ snapshots: async-function@1.0.0: {} + atomic-sleep@1.0.0: {} + attr-accept@2.2.5: {} available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + axe-core@4.11.1: {} axobject-query@4.1.0: {} @@ -11232,8 +13288,26 @@ snapshots: base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.0: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -11259,6 +13333,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -11286,6 +13365,14 @@ snapshots: ccount@2.0.1: {} + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + + chalk-template@1.1.2: + dependencies: + chalk: 5.6.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -11307,10 +13394,18 @@ snapshots: character-reference-invalid@2.0.1: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chokidar@5.0.0: dependencies: readdirp: 5.0.0 + chownr@1.1.4: {} + + cjs-module-lexer@1.4.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -11319,6 +13414,21 @@ snapshots: client-only@0.0.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cloudevents@10.0.0: + dependencies: + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + json-bigint: 1.0.0 + process: 0.11.10 + util: 0.12.5 + uuid: 8.3.2 + clsx@2.1.1: {} cmdk@0.2.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -11353,16 +13463,36 @@ snapshots: comma-separated-tokens@2.0.3: {} + command-line-args@6.0.2: + dependencies: + array-back: 6.2.3 + find-replace: 5.0.2 + lodash.camelcase: 4.3.0 + typical: 7.3.0 + + command-line-usage@7.0.4: + dependencies: + array-back: 6.2.3 + chalk-template: 0.4.0 + table-layout: 4.1.1 + typical: 7.3.0 + + commander@11.1.0: {} + commander@7.2.0: {} commander@8.3.0: {} + compare-utf8@0.1.1: {} + compute-scroll-into-view@3.1.1: {} concat-map@0.0.1: {} convert-source-map@2.0.0: {} + cookie@1.1.1: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -11472,6 +13602,12 @@ snapshots: dependencies: character-entities: 2.0.2 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -11488,6 +13624,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.4: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -11573,6 +13711,20 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + eciesjs@0.4.18: + dependencies: + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + electron-to-chromium@1.5.302: {} emblor@1.4.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -11591,8 +13743,14 @@ snapshots: - '@types/react' - '@types/react-dom' + emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -12054,10 +14212,28 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + expand-template@2.0.3: {} + extend@3.0.2: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -12070,8 +14246,43 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.0: {} + + fastify-plugin@5.1.0: {} + + fastify@5.8.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -12094,10 +14305,20 @@ snapshots: dependencies: tslib: 2.8.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 + + find-replace@5.0.2: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -12120,6 +14341,8 @@ snapshots: format@0.2.2: {} + forwarded-parse@2.1.2: {} + framer-motion@12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: motion-dom: 12.34.3 @@ -12129,6 +14352,8 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + fs-constants@1.0.0: {} + fsevents@2.3.3: optional: true @@ -12248,6 +14473,26 @@ snapshots: functions-have-names@1.2.3: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + geist@1.7.0(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -12256,6 +14501,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -12276,6 +14523,8 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@6.0.1: {} + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -12286,6 +14535,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: {} + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -12303,6 +14554,8 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -12518,12 +14771,23 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https@1.0.0: {} + human-signals@2.1.0: {} + icu-minify@4.8.3: dependencies: '@formatjs/icu-messageformat-parser': 3.5.1 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -12543,10 +14807,19 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} inherits@2.0.4: {} + ini@1.3.8: {} + inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -12562,6 +14835,8 @@ snapshots: '@formatjs/icu-messageformat-parser': 3.5.1 tslib: 2.8.1 + ipaddr.js@2.3.0: {} + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -12576,6 +14851,11 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -12632,6 +14912,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -12650,6 +14932,8 @@ snapshots: is-hotkey@0.2.0: {} + is-in-subnet@4.0.1: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -12678,6 +14962,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -12710,6 +14996,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.5: {} + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -12721,6 +15009,8 @@ snapshots: jiti@2.6.1: {} + jose@5.10.0: {} + jotai-optics@0.4.0(jotai@2.8.4(@types/react@19.2.14)(react@19.2.4))(optics-ts@2.4.1): dependencies: jotai: 2.8.4(@types/react@19.2.14)(react@19.2.4) @@ -12755,18 +15045,32 @@ snapshots: js-tokens@4.0.0: {} + js-xxhash@4.0.0: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} + json-custom-numbers@3.1.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -12797,6 +15101,8 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + kasi@1.1.2: {} + katex@0.16.22: dependencies: commander: 8.3.0 @@ -12828,6 +15134,12 @@ snapshots: dependencies: immediate: 3.0.6 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.31.1: optional: true @@ -12883,6 +15195,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} lodash.mapvalues@4.6.0: {} @@ -13124,6 +15438,8 @@ snapshots: '@types/dom-mediacapture-transform': 0.1.11 '@types/dom-webcodecs': 0.1.13 + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -13405,6 +15721,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-fn@2.1.0: {} + + mimic-response@3.1.0: {} + minimatch@3.1.3: dependencies: brace-expansion: 1.1.12 @@ -13415,6 +15735,14 @@ snapshots: minimist@1.2.8: {} + mkdirp-classic@0.5.3: {} + + mnemonist@0.40.0: + dependencies: + obliterator: 2.0.5 + + module-details-from-path@1.0.4: {} + motion-dom@12.34.3: dependencies: motion-utils: 12.29.2 @@ -13437,6 +15765,8 @@ snapshots: nanoid@5.1.6: {} + napi-build-utils@2.0.0: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -13497,6 +15827,10 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-abi@3.89.0: + dependencies: + semver: 7.7.4 + node-addon-api@7.1.1: {} node-exports-info@1.6.0: @@ -13506,8 +15840,16 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.27: {} + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + npm-to-yarn@3.0.1: {} nth-check@2.1.1: @@ -13524,6 +15866,8 @@ snapshots: object-keys@1.1.1: {} + object-treeify@1.1.33: {} + object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -13560,6 +15904,18 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obliterator@2.0.5: {} + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -13632,6 +15988,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-prometheus-text-format@1.1.1: + dependencies: + shallow-equal: 1.2.1 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -13653,6 +16013,8 @@ snapshots: pg-connection-string@2.11.0: {} + pg-format-fix@1.0.5: {} + pg-int8@1.0.1: {} pg-pool@3.11.0(pg@8.18.0): @@ -13689,6 +16051,26 @@ snapshots: picomatch@4.0.3: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + platejs@52.0.17(@types/react@19.2.14)(immer@10.2.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.4)): dependencies: '@platejs/core': 52.0.17(@types/react@19.2.14)(immer@10.2.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -13743,6 +16125,8 @@ snapshots: dependencies: xtend: 4.0.2 + postgres@3.4.7: {} + postgres@3.4.8: {} posthog-js@1.352.1: @@ -13774,14 +16158,37 @@ snapshots: preact@10.28.4: {} + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.89.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} + prettier@3.8.1: {} + prismjs@1.27.0: {} prismjs@1.30.0: {} process-nextick-args@2.0.1: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + process@0.11.10: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -13811,6 +16218,11 @@ snapshots: proxy-compare@2.6.0: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} query-selector-shadow-dom@1.0.1: {} @@ -13821,6 +16233,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@radix-ui/primitive': 1.1.3 @@ -13888,6 +16302,13 @@ snapshots: dependencies: performance-now: 2.1.0 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-compiler-runtime@1.0.0(react@19.2.4): dependencies: react: 19.2.4 @@ -14059,8 +16480,18 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + readdirp@5.0.0: {} + real-require@0.2.0: {} + recma-build-jsx@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -14250,6 +16681,18 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -14269,8 +16712,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -14327,6 +16774,12 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + scheduler@0.27.0: {} scroll-into-view-if-needed@3.1.0: @@ -14343,6 +16796,8 @@ snapshots: server-only@0.0.1: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -14367,6 +16822,8 @@ snapshots: setimmediate@1.0.5: {} + shallow-equal@1.2.1: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -14444,6 +16901,16 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + slate-dom@0.119.0(slate@0.120.0): dependencies: '@juggle/resize-observer': 3.4.0 @@ -14491,6 +16958,10 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -14520,6 +16991,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + stream-shift@1.0.3: {} + streamdown@2.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: clsx: 2.1.1 @@ -14542,6 +17015,12 @@ snapshots: transitivePeerDependencies: - supports-color + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string.prototype.codepointat@0.2.1: {} string.prototype.includes@2.0.1: @@ -14603,8 +17082,16 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-bom@3.0.0: {} + strip-final-newline@2.0.0: {} + + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} style-to-js@1.1.21: @@ -14648,6 +17135,11 @@ snapshots: tabbable@6.4.0: {} + table-layout@4.1.1: + dependencies: + array-back: 6.2.3 + wordwrapjs: 5.1.1 + tailwind-merge@3.5.0: {} tailwind-scrollbar-hide@4.0.0(tailwindcss@4.2.1): @@ -14662,10 +17154,29 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + text-segmentation@1.0.3: dependencies: utrie: 1.0.2 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + throttleit@2.1.0: {} tiny-inflate@1.0.3: {} @@ -14685,8 +17196,12 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toggle-selection@1.0.6: {} + tr46@0.0.3: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -14717,6 +17232,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.1.2 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -14756,6 +17275,8 @@ snapshots: typescript@5.9.3: {} + typical@7.3.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -14859,6 +17380,10 @@ snapshots: dependencies: punycode: 2.3.1 + url-pattern@1.0.3: {} + + urlpattern-polyfill@10.1.0: {} + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 @@ -14923,10 +17448,22 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.20 + utrie@1.0.2: dependencies: base64-arraybuffer: 1.0.2 + uuid@8.3.2: {} + + uuid@9.0.1: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -14970,6 +17507,13 @@ snapshots: web-vitals@5.1.0: {} + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -15015,12 +17559,42 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.5 + word-wrap@1.2.5: {} + wordwrapjs@5.1.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.20.0: {} + xtend@4.0.2: {} + y18n@5.0.8: {} + yallist@3.1.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zod-to-json-schema@3.25.1(zod@4.3.6): diff --git a/surfsense_web/types/zero.d.ts b/surfsense_web/types/zero.d.ts new file mode 100644 index 000000000..69c9e2402 --- /dev/null +++ b/surfsense_web/types/zero.d.ts @@ -0,0 +1,14 @@ +import type { Schema } from "@/zero/schema/index"; + +export type Context = + | { + userId: string; + } + | undefined; + +declare module "@rocicorp/zero" { + interface DefaultTypes { + context: Context; + schema: Schema; + } +} diff --git a/surfsense_web/zero/queries/chat.ts b/surfsense_web/zero/queries/chat.ts new file mode 100644 index 000000000..de8b13f8a --- /dev/null +++ b/surfsense_web/zero/queries/chat.ts @@ -0,0 +1,21 @@ +import { defineQuery } from "@rocicorp/zero"; +import { z } from "zod"; +import { zql } from "../schema/index"; + +export const messageQueries = { + byThread: defineQuery(z.object({ threadId: z.number() }), ({ args: { threadId } }) => + zql.new_chat_messages.where("threadId", threadId).orderBy("createdAt", "asc") + ), +}; + +export const commentQueries = { + byThread: defineQuery(z.object({ threadId: z.number() }), ({ args: { threadId } }) => + zql.chat_comments.where("threadId", threadId).orderBy("createdAt", "asc") + ), +}; + +export const chatSessionQueries = { + byThread: defineQuery(z.object({ threadId: z.number() }), ({ args: { threadId } }) => + zql.chat_session_state.where("threadId", threadId).one() + ), +}; diff --git a/surfsense_web/zero/queries/documents.ts b/surfsense_web/zero/queries/documents.ts new file mode 100644 index 000000000..97088945f --- /dev/null +++ b/surfsense_web/zero/queries/documents.ts @@ -0,0 +1,15 @@ +import { defineQuery } from "@rocicorp/zero"; +import { z } from "zod"; +import { zql } from "../schema/index"; + +export const documentQueries = { + bySpace: defineQuery(z.object({ searchSpaceId: z.number() }), ({ args: { searchSpaceId } }) => + zql.documents.where("searchSpaceId", searchSpaceId).orderBy("createdAt", "desc") + ), +}; + +export const connectorQueries = { + bySpace: defineQuery(z.object({ searchSpaceId: z.number() }), ({ args: { searchSpaceId } }) => + zql.search_source_connectors.where("searchSpaceId", searchSpaceId).orderBy("createdAt", "desc") + ), +}; diff --git a/surfsense_web/zero/queries/inbox.ts b/surfsense_web/zero/queries/inbox.ts new file mode 100644 index 000000000..d85b7212f --- /dev/null +++ b/surfsense_web/zero/queries/inbox.ts @@ -0,0 +1,9 @@ +import { defineQuery } from "@rocicorp/zero"; +import { z } from "zod"; +import { zql } from "../schema/index"; + +export const notificationQueries = { + byUser: defineQuery(z.object({ userId: z.string() }), ({ args: { userId } }) => + zql.notifications.where("userId", userId).orderBy("createdAt", "desc") + ), +}; diff --git a/surfsense_web/zero/queries/index.ts b/surfsense_web/zero/queries/index.ts new file mode 100644 index 000000000..893e677c4 --- /dev/null +++ b/surfsense_web/zero/queries/index.ts @@ -0,0 +1,13 @@ +import { defineQueries } from "@rocicorp/zero"; +import { chatSessionQueries, commentQueries, messageQueries } from "./chat"; +import { connectorQueries, documentQueries } from "./documents"; +import { notificationQueries } from "./inbox"; + +export const queries = defineQueries({ + notifications: notificationQueries, + documents: documentQueries, + connectors: connectorQueries, + messages: messageQueries, + comments: commentQueries, + chatSession: chatSessionQueries, +}); diff --git a/surfsense_web/zero/schema/chat.ts b/surfsense_web/zero/schema/chat.ts new file mode 100644 index 000000000..0293059fd --- /dev/null +++ b/surfsense_web/zero/schema/chat.ts @@ -0,0 +1,34 @@ +import { json, number, string, table } from "@rocicorp/zero"; + +export const newChatMessageTable = table("new_chat_messages") + .columns({ + id: number(), + role: string(), + content: json(), + threadId: number().from("thread_id"), + authorId: string().optional().from("author_id"), + createdAt: number().from("created_at"), + }) + .primaryKey("id"); + +export const chatCommentTable = table("chat_comments") + .columns({ + id: number(), + messageId: number().from("message_id"), + threadId: number().from("thread_id"), + parentId: number().optional().from("parent_id"), + authorId: string().optional().from("author_id"), + content: string(), + createdAt: number().from("created_at"), + updatedAt: number().from("updated_at"), + }) + .primaryKey("id"); + +export const chatSessionStateTable = table("chat_session_state") + .columns({ + id: number(), + threadId: number().from("thread_id"), + aiRespondingToUserId: string().optional().from("ai_responding_to_user_id"), + updatedAt: number().from("updated_at"), + }) + .primaryKey("id"); diff --git a/surfsense_web/zero/schema/documents.ts b/surfsense_web/zero/schema/documents.ts new file mode 100644 index 000000000..ceeefd877 --- /dev/null +++ b/surfsense_web/zero/schema/documents.ts @@ -0,0 +1,31 @@ +import { boolean, json, number, string, table } from "@rocicorp/zero"; + +export const documentTable = table("documents") + .columns({ + id: number(), + title: string(), + documentType: string().from("document_type"), + searchSpaceId: number().from("search_space_id"), + createdById: string().optional().from("created_by_id"), + status: json(), + createdAt: number().from("created_at"), + }) + .primaryKey("id"); + +export const searchSourceConnectorTable = table("search_source_connectors") + .columns({ + id: number(), + name: string(), + connectorType: string().from("connector_type"), + isIndexable: boolean().from("is_indexable"), + lastIndexedAt: number().optional().from("last_indexed_at"), + config: json(), + enableSummary: boolean().from("enable_summary"), + periodicIndexingEnabled: boolean().from("periodic_indexing_enabled"), + indexingFrequencyMinutes: number().optional().from("indexing_frequency_minutes"), + nextScheduledAt: number().optional().from("next_scheduled_at"), + searchSpaceId: number().from("search_space_id"), + userId: string().from("user_id"), + createdAt: number().from("created_at"), + }) + .primaryKey("id"); diff --git a/surfsense_web/zero/schema/inbox.ts b/surfsense_web/zero/schema/inbox.ts new file mode 100644 index 000000000..946180ba4 --- /dev/null +++ b/surfsense_web/zero/schema/inbox.ts @@ -0,0 +1,16 @@ +import { boolean, json, number, string, table } from "@rocicorp/zero"; + +export const notificationTable = table("notifications") + .columns({ + id: number(), + userId: string().from("user_id"), + searchSpaceId: number().optional().from("search_space_id"), + type: string(), + title: string(), + message: string(), + read: boolean(), + metadata: json().optional(), + createdAt: number().from("created_at"), + updatedAt: number().optional().from("updated_at"), + }) + .primaryKey("id"); diff --git a/surfsense_web/zero/schema/index.ts b/surfsense_web/zero/schema/index.ts new file mode 100644 index 000000000..0a6587e92 --- /dev/null +++ b/surfsense_web/zero/schema/index.ts @@ -0,0 +1,41 @@ +import { createBuilder, createSchema, relationships } from "@rocicorp/zero"; +import { chatCommentTable, chatSessionStateTable, newChatMessageTable } from "./chat"; +import { documentTable, searchSourceConnectorTable } from "./documents"; +import { notificationTable } from "./inbox"; + +const chatCommentRelationships = relationships(chatCommentTable, ({ one }) => ({ + message: one({ + sourceField: ["messageId"], + destSchema: newChatMessageTable, + destField: ["id"], + }), + parent: one({ + sourceField: ["parentId"], + destSchema: chatCommentTable, + destField: ["id"], + }), +})); + +const newChatMessageRelationships = relationships(newChatMessageTable, ({ many }) => ({ + comments: many({ + sourceField: ["id"], + destSchema: chatCommentTable, + destField: ["messageId"], + }), +})); + +export const schema = createSchema({ + tables: [ + notificationTable, + documentTable, + searchSourceConnectorTable, + newChatMessageTable, + chatCommentTable, + chatSessionStateTable, + ], + relationships: [chatCommentRelationships, newChatMessageRelationships], +}); + +export type Schema = typeof schema; + +export const zql = createBuilder(schema);