From 2b7465cdaacfabeb5be8662a7cb60fa3d4eca948 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 16:40:25 +0200 Subject: [PATCH 01/31] chore: remove Electric SQL plumbing and infrastructure Remove all Electric SQL client code, Docker service, env vars, CI build args, install scripts, and documentation. Feature hooks that depend on Electric are intentionally left in place to be rewritten with Rocicorp Zero in subsequent commits. Deleted: - lib/electric/ (client.ts, context.ts, auth.ts, baseline.ts) - ElectricProvider.tsx - docker/scripts/init-electric-user.sh - content/docs/how-to/electric-sql.mdx Cleaned: - package.json (4 @electric-sql/* deps) - app/layout.tsx, UserDropdown.tsx, LayoutDataProvider.tsx - docker-compose.yml, docker-compose.dev.yml - Dockerfile, docker-entrypoint.js - .env.example (frontend, docker, backend) - CI workflows, install scripts, docs --- .github/workflows/desktop-release.yml | 1 - .github/workflows/docker-build.yml | 2 - docker/.env.example | 16 - docker/docker-compose.dev.yml | 29 - docker/docker-compose.yml | 29 - docker/scripts/init-electric-user.sh | 38 - docker/scripts/install.ps1 | 1 - docker/scripts/install.sh | 2 - surfsense_backend/.env.example | 4 - surfsense_backend/alembic/env.py | 7 - surfsense_web/.env.example | 4 - surfsense_web/Dockerfile | 4 - surfsense_web/app/layout.tsx | 9 +- surfsense_web/app/sitemap.ts | 6 - surfsense_web/components/UserDropdown.tsx | 9 - .../layout/providers/LayoutDataProvider.tsx | 11 - .../components/providers/ElectricProvider.tsx | 116 --- .../docs/docker-installation/dev-compose.mdx | 1 - .../docker-installation/docker-compose.mdx | 16 - .../docker-installation/install-script.mdx | 1 - .../content/docs/how-to/electric-sql.mdx | 226 ----- surfsense_web/content/docs/how-to/index.mdx | 5 - surfsense_web/content/docs/how-to/meta.json | 2 +- .../content/docs/manual-installation.mdx | 4 - surfsense_web/docker-entrypoint.js | 2 - surfsense_web/lib/electric/auth.ts | 20 - surfsense_web/lib/electric/baseline.ts | 62 -- surfsense_web/lib/electric/client.ts | 848 ------------------ surfsense_web/lib/electric/context.ts | 36 - surfsense_web/package.json | 4 - 30 files changed, 4 insertions(+), 1511 deletions(-) delete mode 100755 docker/scripts/init-electric-user.sh delete mode 100644 surfsense_web/components/providers/ElectricProvider.tsx delete mode 100644 surfsense_web/content/docs/how-to/electric-sql.mdx delete mode 100644 surfsense_web/lib/electric/auth.ts delete mode 100644 surfsense_web/lib/electric/baseline.ts delete mode 100644 surfsense_web/lib/electric/client.ts delete mode 100644 surfsense_web/lib/electric/context.ts diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 7119fcb6d..b431f7ca2 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -57,7 +57,6 @@ 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_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..cedfe9d32 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -164,8 +164,6 @@ 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_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__' || '' }} - name: Export digest diff --git a/docker/.env.example b/docker/.env.example index a226c2624..e8c79425c 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -35,7 +35,6 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # BACKEND_PORT=8929 # FRONTEND_PORT=3929 -# ELECTRIC_PORT=5929 # SEARXNG_PORT=8888 # FLOWER_PORT=5555 @@ -58,7 +57,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,7 +69,6 @@ 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 # ------------------------------------------------------------------------------ @@ -101,19 +98,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..9eaaeedae 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,24 +169,6 @@ services: # - redis # - celery_worker - electric: - image: electricsql/electric:1.4.10 - ports: - - "${ELECTRIC_PORT:-5133}:3000" - 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 - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"] - interval: 10s - timeout: 5s - retries: 5 - frontend: build: context: ../surfsense_web @@ -201,8 +176,6 @@ 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_DEPLOYMENT_MODE: ${NEXT_PUBLIC_DEPLOYMENT_MODE:-self-hosted} ports: - "${FRONTEND_PORT:-3000}:3000" @@ -211,8 +184,6 @@ services: depends_on: backend: condition: service_healthy - electric: - condition: service_healthy volumes: postgres_data: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8c85248d2..12961654a 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,42 +158,20 @@ services: # - celery_worker # restart: unless-stopped - electric: - image: electricsql/electric:1.4.10 - ports: - - "${ELECTRIC_PORT:-5929}:3000" - 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 - restart: unless-stopped - depends_on: - db: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/v1/health"] - interval: 10s - timeout: 5s - retries: 5 - frontend: image: ghcr.io/modsetter/surfsense-web:${SURFSENSE_VERSION:-latest} ports: - "${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_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: - condition: service_healthy restart: unless-stopped volumes: 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 621c8cf99..68fada394 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_web/.env.example b/surfsense_web/.env.example index d390a0cb7..7d0e888d1 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -2,10 +2,6 @@ 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 - # 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..e1d231fac 100644 --- a/surfsense_web/Dockerfile +++ b/surfsense_web/Dockerfile @@ -35,15 +35,11 @@ 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_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_DEPLOYMENT_MODE=$NEXT_PUBLIC_DEPLOYMENT_MODE COPY --from=deps /app/node_modules ./node_modules diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index b0aa58a8f..f679bd968 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -3,7 +3,6 @@ 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"; @@ -140,11 +139,9 @@ export default function RootLayout({ defaultTheme="system" > - - - {children} - - + + {children} + diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts index 5a11ef3fc..4470f53bf 100644 --- a/surfsense_web/app/sitemap.ts +++ b/surfsense_web/app/sitemap.ts @@ -212,12 +212,6 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.8, }, // How-to documentation - { - url: "https://www.surfsense.com/docs/how-to/electric-sql", - lastModified, - changeFrequency: "daily", - priority: 0.8, - }, { url: "https://www.surfsense.com/docs/how-to/realtime-collaboration", lastModified, 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/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 6ae258f99..47a2b2362 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -51,7 +51,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 { MorePagesDialog } from "@/components/settings/more-pages-dialog"; @@ -159,8 +158,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; @@ -607,14 +604,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 aded9533a..000000000 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ /dev/null @@ -1,116 +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"; - -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()) { - 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 { - console.log(`[ElectricProvider] Initializing for user: ${userId}`); - const client = await initElectric(userId); - - if (mounted) { - previousUserIdRef.current = userId; - setElectricClient(client); - setError(null); - 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/content/docs/docker-installation/dev-compose.mdx b/surfsense_web/content/docs/docker-installation/dev-compose.mdx index 302026c2a..5c800e450 100644 --- a/surfsense_web/content/docs/docker-installation/dev-compose.mdx +++ b/surfsense_web/content/docs/docker-installation/dev-compose.mdx @@ -25,6 +25,5 @@ The following `.env` variables are **only used by the dev compose file** (they h | `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_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..bf0ca762f 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,6 @@ 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` | ### Custom Domain / Reverse Proxy @@ -61,7 +58,6 @@ 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`) | ### Database @@ -77,14 +73,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 +136,6 @@ 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) | | `frontend` | Next.js web application | All services start automatically with `docker compose up -d`. @@ -165,7 +152,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 +169,4 @@ 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. - **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..f3f932c4c 100644 --- a/surfsense_web/content/docs/docker-installation/install-script.mdx +++ b/surfsense_web/content/docs/docker-installation/install-script.mdx @@ -38,4 +38,3 @@ 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) 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..a6571036d 100644 --- a/surfsense_web/content/docs/how-to/index.mdx +++ b/surfsense_web/content/docs/how-to/index.mdx @@ -8,11 +8,6 @@ import { Card, Cards } from 'fumadocs-ui/components/card'; Practical guides to help you get the most out of SurfSense. - { - // 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/package.json b/surfsense_web/package.json index f2a401f07..52350325b 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", From 8298aad2d7fa5ba68b6b7743871b90e4a6657bec Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 17:23:49 +0200 Subject: [PATCH 02/31] feat: add @rocicorp/zero package and auth context type - Install @rocicorp/zero@0.26.2 (replaces 4 @electric-sql/* packages) - Add types/zero-auth.d.ts with Context type for Zero's permission system --- surfsense_web/package.json | 1 + surfsense_web/pnpm-lock.yaml | 2694 +++++++++++++++++++++++++++- surfsense_web/types/zero-auth.d.ts | 11 + 3 files changed, 2646 insertions(+), 60 deletions(-) create mode 100644 surfsense_web/types/zero-auth.d.ts diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 52350325b..54ecfe92b 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -72,6 +72,7 @@ "@remotion/media": "^4.0.438", "@remotion/player": "^4.0.438", "@remotion/web-renderer": "^4.0.438", + "@rocicorp/zero": "^0.26.2", "@streamdown/code": "^1.0.2", "@streamdown/math": "^1.0.2", "@tabler/icons-react": "^3.34.1", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index 806d7e981..d6a3b77b7 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)) '@streamdown/code': specifier: ^1.0.2 version: 1.0.3(react@19.2.4) @@ -1111,6 +1102,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'} @@ -1164,36 +1159,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==} @@ -1693,6 +1689,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==} @@ -1737,6 +1757,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: @@ -1911,6 +1944,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==} @@ -1932,9 +1968,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==} @@ -1992,6 +2025,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'} @@ -2014,6 +2059,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'} @@ -2022,6 +2071,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} @@ -2034,24 +2108,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} @@ -2064,28 +2526,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'} @@ -2172,6 +2688,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: @@ -2308,6 +2827,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==} @@ -3390,6 +3913,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] @@ -3859,6 +4411,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==} @@ -3874,9 +4429,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==} @@ -3922,15 +4486,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==} @@ -3945,6 +4524,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==} @@ -3954,6 +4536,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} @@ -4142,6 +4727,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: @@ -4152,6 +4745,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'} @@ -4168,9 +4765,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'} @@ -4186,6 +4806,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'} @@ -4238,6 +4862,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'} @@ -4246,6 +4874,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'} @@ -4283,11 +4914,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==} @@ -4310,6 +4957,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'} @@ -4339,6 +4989,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'} @@ -4368,10 +5026,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==} @@ -4381,6 +5049,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'} @@ -4413,6 +5089,23 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + command-line-args@6.0.1: + resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} + 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'} @@ -4421,6 +5114,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==} @@ -4430,6 +5126,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==} @@ -4534,6 +5234,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==} @@ -4549,6 +5257,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'} @@ -4703,6 +5414,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==} @@ -4712,9 +5430,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'} @@ -4947,13 +5671,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==} @@ -4964,9 +5702,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==} @@ -4993,10 +5746,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'} @@ -5019,6 +5788,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: @@ -5033,6 +5805,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} @@ -5152,6 +5927,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: @@ -5165,6 +5948,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'} @@ -5177,6 +5964,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'} @@ -5184,6 +5975,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==} @@ -5203,6 +5997,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'} @@ -5317,12 +6115,23 @@ packages: resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} engines: {node: '>=8.0.0'} + 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'} @@ -5351,6 +6160,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'} @@ -5358,6 +6170,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==} @@ -5368,6 +6183,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==} @@ -5380,6 +6199,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'} @@ -5432,6 +6255,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'} @@ -5449,6 +6276,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'} @@ -5485,6 +6316,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'} @@ -5518,6 +6353,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'} @@ -5526,6 +6365,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: @@ -5590,6 +6432,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 @@ -5599,15 +6445,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==} @@ -5635,6 +6493,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 @@ -5674,6 +6535,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'} @@ -5751,6 +6615,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==} @@ -5879,6 +6746,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'} @@ -5995,6 +6865,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==} @@ -6005,6 +6883,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==} @@ -6042,6 +6929,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} @@ -6097,6 +6987,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==} @@ -6104,9 +6998,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} @@ -6129,6 +7036,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'} @@ -6149,6 +7060,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==} @@ -6199,6 +7124,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==} @@ -6229,6 +7157,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'} @@ -6268,6 +7200,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: @@ -6313,6 +7255,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'} @@ -6330,10 +7276,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'} @@ -6345,6 +7302,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==} @@ -6361,6 +7328,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'} @@ -6374,6 +7344,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: @@ -6390,6 +7363,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: @@ -6550,10 +7527,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==} @@ -6654,6 +7643,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'} @@ -6671,10 +7672,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'} @@ -6698,6 +7706,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==} @@ -6722,6 +7738,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'} @@ -6737,6 +7756,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} @@ -6768,6 +7790,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: @@ -6792,6 +7823,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: @@ -6830,12 +7864,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==} @@ -6868,10 +7909,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'} @@ -6919,6 +7972,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==} @@ -6939,9 +7996,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'} @@ -6964,9 +8032,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==} @@ -6998,6 +8073,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'} @@ -7023,6 +8101,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'} @@ -7088,6 +8170,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'} @@ -7171,9 +8260,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: @@ -7235,6 +8335,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'} @@ -7256,17 +8362,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'} @@ -8173,6 +9319,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 @@ -8208,36 +9356,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: @@ -8533,6 +9683,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 @@ -8591,6 +9778,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 @@ -8723,6 +9924,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': @@ -8767,8 +9970,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 @@ -8806,6 +10007,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 @@ -8827,12 +10036,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 @@ -8843,6 +10125,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 @@ -8852,12 +10158,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 @@ -8869,6 +10635,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 @@ -8881,6 +10702,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 @@ -8888,12 +10722,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 @@ -8901,8 +10776,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': @@ -8965,6 +10866,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 @@ -9170,6 +11073,8 @@ snapshots: - scheduler - use-sync-external-store + '@postgresql-typed/oids@0.2.0': {} + '@posthog/core@1.23.1': dependencies: cross-spawn: 7.0.6 @@ -10307,6 +12212,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.1 + 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 @@ -10713,6 +12691,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 @@ -10743,8 +12723,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 @@ -10787,8 +12779,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 @@ -10797,6 +12797,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 @@ -10815,6 +12829,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 @@ -10822,6 +12840,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 @@ -11008,12 +13030,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 @@ -11034,6 +13064,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 @@ -11041,6 +13079,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 @@ -11053,6 +13100,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 @@ -11142,12 +13191,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: {} @@ -11184,8 +13240,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: @@ -11211,6 +13285,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 @@ -11238,6 +13317,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 @@ -11259,10 +13346,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 @@ -11271,6 +13366,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): @@ -11305,16 +13415,36 @@ snapshots: comma-separated-tokens@2.0.3: {} + command-line-args@6.0.1: + 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 @@ -11416,6 +13546,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: {} @@ -11432,6 +13568,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: {} @@ -11517,6 +13655,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): @@ -11535,8 +13687,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 @@ -11998,10 +14156,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: @@ -12014,8 +14190,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 @@ -12038,10 +14249,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 @@ -12064,6 +14285,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 @@ -12073,6 +14296,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 @@ -12192,6 +14417,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) @@ -12200,6 +14445,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 @@ -12220,6 +14467,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 @@ -12230,6 +14479,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: @@ -12247,6 +14498,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: {} @@ -12455,12 +14708,23 @@ snapshots: css-line-break: 2.1.0 text-segmentation: 1.0.3 + 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: {} @@ -12480,10 +14744,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: @@ -12499,6 +14772,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: {} @@ -12513,6 +14788,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 @@ -12569,6 +14849,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 @@ -12587,6 +14869,8 @@ snapshots: is-hotkey@0.2.0: {} + is-in-subnet@4.0.1: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -12615,6 +14899,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -12647,6 +14933,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.5: {} + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -12658,6 +14946,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) @@ -12692,18 +14982,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: {} @@ -12734,6 +15038,8 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + kasi@1.1.2: {} + katex@0.16.22: dependencies: commander: 8.3.0 @@ -12765,6 +15071,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 @@ -12820,6 +15132,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} lodash.mapvalues@4.6.0: {} @@ -13061,6 +15375,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: @@ -13342,6 +15658,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 @@ -13352,6 +15672,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 @@ -13374,6 +15702,8 @@ snapshots: nanoid@5.1.6: {} + napi-build-utils@2.0.0: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -13434,6 +15764,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: @@ -13443,8 +15777,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: @@ -13461,6 +15803,8 @@ snapshots: object-keys@1.1.1: {} + object-treeify@1.1.33: {} + object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -13497,6 +15841,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: @@ -13569,6 +15925,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 @@ -13590,6 +15950,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): @@ -13626,6 +15988,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)) @@ -13680,6 +16062,8 @@ snapshots: dependencies: xtend: 4.0.2 + postgres@3.4.7: {} + postgres@3.4.8: {} posthog-js@1.352.1: @@ -13711,14 +16095,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 @@ -13748,6 +16155,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: {} @@ -13758,6 +16170,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 @@ -13825,6 +16239,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 @@ -13996,8 +16417,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 @@ -14187,6 +16618,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: {} @@ -14206,8 +16649,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 @@ -14264,6 +16711,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: @@ -14280,6 +16733,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 @@ -14304,6 +16759,8 @@ snapshots: setimmediate@1.0.5: {} + shallow-equal@1.2.1: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -14381,6 +16838,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 @@ -14417,6 +16884,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 @@ -14446,6 +16917,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 @@ -14468,6 +16941,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: @@ -14529,8 +17008,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: @@ -14574,6 +17061,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): @@ -14588,10 +17080,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: {} @@ -14609,8 +17120,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: {} @@ -14639,6 +17154,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 @@ -14678,6 +17197,8 @@ snapshots: typescript@5.9.3: {} + typical@7.3.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -14781,6 +17302,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 @@ -14845,10 +17370,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) @@ -14892,6 +17429,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 @@ -14937,12 +17481,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-auth.d.ts b/surfsense_web/types/zero-auth.d.ts new file mode 100644 index 000000000..7d5007b23 --- /dev/null +++ b/surfsense_web/types/zero-auth.d.ts @@ -0,0 +1,11 @@ +export type Context = + | { + userId: string; + } + | undefined; + +declare module "@rocicorp/zero" { + interface DefaultTypes { + context: Context; + } +} From af2bd744fba7a09f4c3bde1503d8c5ddb265a83a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 17:44:05 +0200 Subject: [PATCH 03/31] feat: add Zero schema with 6 table definitions and relationships - Create zero/tables/inbox.ts (notifications) - Create zero/tables/documents.ts (documents, search_source_connectors) - Create zero/tables/chat.ts (new_chat_messages, chat_comments, chat_session_state) - Create zero/schema.ts (combines tables, defines relationships, exports zql) - Consolidate Zero type augmentations into types/zero.d.ts --- .../types/{zero-auth.d.ts => zero.d.ts} | 3 ++ surfsense_web/zero/schema.ts | 41 +++++++++++++++++++ surfsense_web/zero/tables/chat.ts | 34 +++++++++++++++ surfsense_web/zero/tables/documents.ts | 31 ++++++++++++++ surfsense_web/zero/tables/inbox.ts | 16 ++++++++ 5 files changed, 125 insertions(+) rename surfsense_web/types/{zero-auth.d.ts => zero.d.ts} (70%) create mode 100644 surfsense_web/zero/schema.ts create mode 100644 surfsense_web/zero/tables/chat.ts create mode 100644 surfsense_web/zero/tables/documents.ts create mode 100644 surfsense_web/zero/tables/inbox.ts diff --git a/surfsense_web/types/zero-auth.d.ts b/surfsense_web/types/zero.d.ts similarity index 70% rename from surfsense_web/types/zero-auth.d.ts rename to surfsense_web/types/zero.d.ts index 7d5007b23..5b74bce33 100644 --- a/surfsense_web/types/zero-auth.d.ts +++ b/surfsense_web/types/zero.d.ts @@ -1,3 +1,5 @@ +import type { Schema } from "@/zero/schema"; + export type Context = | { userId: string; @@ -7,5 +9,6 @@ export type Context = declare module "@rocicorp/zero" { interface DefaultTypes { context: Context; + schema: Schema; } } diff --git a/surfsense_web/zero/schema.ts b/surfsense_web/zero/schema.ts new file mode 100644 index 000000000..c1f7cf951 --- /dev/null +++ b/surfsense_web/zero/schema.ts @@ -0,0 +1,41 @@ +import { createSchema, createBuilder, relationships } from "@rocicorp/zero"; +import { chatCommentTable, chatSessionStateTable, newChatMessageTable } from "./tables/chat"; +import { documentTable, searchSourceConnectorTable } from "./tables/documents"; +import { notificationTable } from "./tables/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); diff --git a/surfsense_web/zero/tables/chat.ts b/surfsense_web/zero/tables/chat.ts new file mode 100644 index 000000000..b8b7fbb93 --- /dev/null +++ b/surfsense_web/zero/tables/chat.ts @@ -0,0 +1,34 @@ +import { table, string, number, json } 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/tables/documents.ts b/surfsense_web/zero/tables/documents.ts new file mode 100644 index 000000000..c50fc959b --- /dev/null +++ b/surfsense_web/zero/tables/documents.ts @@ -0,0 +1,31 @@ +import { table, string, number, boolean, json } 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/tables/inbox.ts b/surfsense_web/zero/tables/inbox.ts new file mode 100644 index 000000000..3eca9522f --- /dev/null +++ b/surfsense_web/zero/tables/inbox.ts @@ -0,0 +1,16 @@ +import { table, string, number, boolean, json } 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"); From da8f90bfe207d90ca5289e24ed8b7a3bbcdd544a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 17:54:43 +0200 Subject: [PATCH 04/31] feat: add Zero queries for all 6 synced tables Define named queries matching each Electric hook's data needs: - notifications.byUser (use-inbox) - documents.bySpace (use-documents, use-documents-processing) - connectors.bySpace (use-connectors-electric) - messages.byThread (use-messages-electric) - comments.byThread (use-comments-electric) - chatSession.byThread (use-chat-session-state) Also moves schema files from zero/tables/ to zero/schema/ for consistent modular folder structure. --- surfsense_web/types/zero.d.ts | 2 +- surfsense_web/zero/queries/chat.ts | 26 +++++++++++++++++++ surfsense_web/zero/queries/documents.ts | 21 +++++++++++++++ surfsense_web/zero/queries/inbox.ts | 11 ++++++++ surfsense_web/zero/queries/index.ts | 13 ++++++++++ surfsense_web/zero/{tables => schema}/chat.ts | 0 .../zero/{tables => schema}/documents.ts | 0 .../zero/{tables => schema}/inbox.ts | 0 .../zero/{schema.ts => schema/index.ts} | 6 ++--- 9 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 surfsense_web/zero/queries/chat.ts create mode 100644 surfsense_web/zero/queries/documents.ts create mode 100644 surfsense_web/zero/queries/inbox.ts create mode 100644 surfsense_web/zero/queries/index.ts rename surfsense_web/zero/{tables => schema}/chat.ts (100%) rename surfsense_web/zero/{tables => schema}/documents.ts (100%) rename surfsense_web/zero/{tables => schema}/inbox.ts (100%) rename surfsense_web/zero/{schema.ts => schema/index.ts} (85%) diff --git a/surfsense_web/types/zero.d.ts b/surfsense_web/types/zero.d.ts index 5b74bce33..69c9e2402 100644 --- a/surfsense_web/types/zero.d.ts +++ b/surfsense_web/types/zero.d.ts @@ -1,4 +1,4 @@ -import type { Schema } from "@/zero/schema"; +import type { Schema } from "@/zero/schema/index"; export type Context = | { diff --git a/surfsense_web/zero/queries/chat.ts b/surfsense_web/zero/queries/chat.ts new file mode 100644 index 000000000..45ffa7ea2 --- /dev/null +++ b/surfsense_web/zero/queries/chat.ts @@ -0,0 +1,26 @@ +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..aa34bf718 --- /dev/null +++ b/surfsense_web/zero/queries/documents.ts @@ -0,0 +1,21 @@ +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..6221ef345 --- /dev/null +++ b/surfsense_web/zero/queries/inbox.ts @@ -0,0 +1,11 @@ +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/tables/chat.ts b/surfsense_web/zero/schema/chat.ts similarity index 100% rename from surfsense_web/zero/tables/chat.ts rename to surfsense_web/zero/schema/chat.ts diff --git a/surfsense_web/zero/tables/documents.ts b/surfsense_web/zero/schema/documents.ts similarity index 100% rename from surfsense_web/zero/tables/documents.ts rename to surfsense_web/zero/schema/documents.ts diff --git a/surfsense_web/zero/tables/inbox.ts b/surfsense_web/zero/schema/inbox.ts similarity index 100% rename from surfsense_web/zero/tables/inbox.ts rename to surfsense_web/zero/schema/inbox.ts diff --git a/surfsense_web/zero/schema.ts b/surfsense_web/zero/schema/index.ts similarity index 85% rename from surfsense_web/zero/schema.ts rename to surfsense_web/zero/schema/index.ts index c1f7cf951..1c3e89151 100644 --- a/surfsense_web/zero/schema.ts +++ b/surfsense_web/zero/schema/index.ts @@ -1,7 +1,7 @@ import { createSchema, createBuilder, relationships } from "@rocicorp/zero"; -import { chatCommentTable, chatSessionStateTable, newChatMessageTable } from "./tables/chat"; -import { documentTable, searchSourceConnectorTable } from "./tables/documents"; -import { notificationTable } from "./tables/inbox"; +import { chatCommentTable, chatSessionStateTable, newChatMessageTable } from "./chat"; +import { documentTable, searchSourceConnectorTable } from "./documents"; +import { notificationTable } from "./inbox"; const chatCommentRelationships = relationships(chatCommentTable, ({ one }) => ({ message: one({ From 6143a91406a90dda63d211851f3801af148297c8 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 18:09:59 +0200 Subject: [PATCH 05/31] feat: add ZeroProvider and wire into app layout - Create components/providers/ZeroProvider.tsx with schema, queries, userID, context, and cacheURL configuration - Wire ZeroProvider into app/layout.tsx wrapping GlobalLoadingProvider inside ReactQueryClientProvider (same position ElectricProvider had) --- surfsense_web/app/layout.tsx | 5 ++++- .../components/providers/ZeroProvider.tsx | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/components/providers/ZeroProvider.tsx diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index f679bd968..9ba06c04d 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -4,6 +4,7 @@ import { RootProvider } from "fumadocs-ui/provider/next"; import { Roboto } from "next/font/google"; import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider"; import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider"; +import { ZeroProvider } from "@/components/providers/ZeroProvider"; import { I18nProvider } from "@/components/providers/I18nProvider"; import { PostHogProvider } from "@/components/providers/PostHogProvider"; import { ThemeProvider } from "@/components/theme/theme-provider"; @@ -140,7 +141,9 @@ export default function RootLayout({ > - {children} + + {children} + diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx new file mode 100644 index 000000000..5e20e9cda --- /dev/null +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { queries } from "@/zero/queries"; +import { schema } from "@/zero/schema"; +import { ZeroProvider as ZeroReactProvider } from "@rocicorp/zero/react"; +import { useAtomValue } from "jotai"; + +const cacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848"; + +export function ZeroProvider({ children }: { children: React.ReactNode }) { + const { data: user } = useAtomValue(currentUserAtom); + const userID = user?.id ? String(user.id) : ""; + const context = user?.id ? { userId: String(user.id) } : undefined; + + return ( + + {children} + + ); +} From 61610106f4c4cce9500434e0ba829556d75a66fc Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 18:19:37 +0200 Subject: [PATCH 06/31] feat: add zero-cache service to production docker-compose - Add rocicorp/zero:0.26.2 service with Postgres connection, SQLite replica volume, healthcheck, and admin password - Add NEXT_PUBLIC_ZERO_CACHE_URL to frontend env vars - Frontend now depends on zero-cache health --- docker/docker-compose.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 12961654a..d659fbd15 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -158,12 +158,35 @@ services: # - celery_worker # restart: unless-stopped + zero-cache: + image: rocicorp/zero:0.26.2 + ports: + - "${ZERO_CACHE_PORT:-5929}:4848" + environment: + 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} + volumes: + - zero_cache_data:/data + restart: unless-stopped + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4848/keepalive"] + interval: 10s + timeout: 5s + retries: 5 + frontend: image: ghcr.io/modsetter/surfsense-web:${SURFSENSE_VERSION:-latest} ports: - "${FRONTEND_PORT:-3929}:3000" environment: NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8929}} + 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} @@ -172,6 +195,8 @@ services: depends_on: backend: condition: service_healthy + zero-cache: + condition: service_healthy restart: unless-stopped volumes: @@ -181,3 +206,5 @@ volumes: name: surfsense-redis shared_temp: name: surfsense-shared-temp + zero_cache_data: + name: surfsense-zero-cache From f9ba7e52d97d6ddfb53cff685f1f0d154c318ac2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 18:27:18 +0200 Subject: [PATCH 07/31] =?UTF-8?q?feat:=20add=20Zero=20infra=20=E2=80=94=20?= =?UTF-8?q?docker-compose,=20env=20vars,=20Dockerfile,=20CI=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add zero-cache service to docker-compose.yml and docker-compose.dev.yml - Add ZERO_* env vars to docker/.env.example - Add NEXT_PUBLIC_ZERO_CACHE_URL to surfsense_web/.env.example - Add NEXT_PUBLIC_ZERO_CACHE_URL placeholder ARG/ENV to Dockerfile - Add NEXT_PUBLIC_ZERO_CACHE_URL runtime substitution to docker-entrypoint.js - Add NEXT_PUBLIC_ZERO_CACHE_URL build arg to docker-build.yml and desktop-release.yml --- .github/workflows/desktop-release.yml | 1 + .github/workflows/docker-build.yml | 1 + docker/.env.example | 14 ++++++++++++++ docker/docker-compose.dev.yml | 27 +++++++++++++++++++++++++++ surfsense_web/.env.example | 1 + surfsense_web/Dockerfile | 2 ++ surfsense_web/docker-entrypoint.js | 1 + 7 files changed, 47 insertions(+) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index b431f7ca2..491df0992 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -57,6 +57,7 @@ jobs: working-directory: surfsense_web env: NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${{ vars.NEXT_PUBLIC_FASTAPI_BACKEND_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 cedfe9d32..2e5de8cc6 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -164,6 +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_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 e8c79425c..04e67de04 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -35,6 +35,7 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # BACKEND_PORT=8929 # FRONTEND_PORT=3929 +# ZERO_CACHE_PORT=5929 # SEARXNG_PORT=8888 # FLOWER_PORT=5555 @@ -69,7 +70,20 @@ 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_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 # ------------------------------------------------------------------------------ # Database (defaults work out of the box, change for security) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 9eaaeedae..b91b95af7 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -169,6 +169,28 @@ services: # - redis # - celery_worker + zero-cache: + image: rocicorp/zero:0.26.2 + ports: + - "${ZERO_CACHE_PORT:-4848}:4848" + depends_on: + db: + condition: service_healthy + environment: + - 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} + volumes: + - zero_cache_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4848/keepalive"] + interval: 10s + timeout: 5s + retries: 5 + frontend: build: context: ../surfsense_web @@ -176,6 +198,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_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" @@ -184,6 +207,8 @@ services: depends_on: backend: condition: service_healthy + zero-cache: + condition: service_healthy volumes: postgres_data: @@ -194,3 +219,5 @@ volumes: name: surfsense-dev-redis shared_temp: name: surfsense-dev-shared-temp + zero_cache_data: + name: surfsense-dev-zero-cache diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example index 7d0e888d1..0524e617c 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -1,6 +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 +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 e1d231fac..da6bc8b7e 100644 --- a/surfsense_web/Dockerfile +++ b/surfsense_web/Dockerfile @@ -35,11 +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_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_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/docker-entrypoint.js b/surfsense_web/docker-entrypoint.js index 0d9bbc389..dad52b73e 100644 --- a/surfsense_web/docker-entrypoint.js +++ b/surfsense_web/docker-entrypoint.js @@ -22,6 +22,7 @@ const replacements = [ 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"], ]; From f29a3edcab19b2e592f486d0724e7df010a9f16a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 18:30:49 +0200 Subject: [PATCH 08/31] chore: update postgresql.conf and contracts comments for Zero - Replace Electric SQL references in postgresql.conf with Zero-cache - Clean Electric SQL comments in chat-comments.types.ts and chat-messages.types.ts --- docker/postgresql.conf | 4 ++-- surfsense_web/contracts/types/chat-comments.types.ts | 2 +- surfsense_web/contracts/types/chat-messages.types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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(), From 6ad5ead3201f2371305589c7e1caef7e504d4176 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 18:35:39 +0200 Subject: [PATCH 09/31] docs: add Zero sync documentation and update existing docs - Create how-to/zero-sync.mdx setup guide - Update docker-compose.mdx with zero-cache service, env vars, troubleshooting - Update dev-compose.mdx with Zero build arg - Update install-script.mdx with zero-cache URL - Update manual-installation.mdx with Zero frontend env var - Add Zero sync page to how-to index, meta.json, and sitemap --- surfsense_web/app/sitemap.ts | 6 ++ .../docs/docker-installation/dev-compose.mdx | 1 + .../docker-installation/docker-compose.mdx | 16 ++++ .../docker-installation/install-script.mdx | 1 + surfsense_web/content/docs/how-to/index.mdx | 5 ++ surfsense_web/content/docs/how-to/meta.json | 2 +- .../content/docs/how-to/zero-sync.mdx | 74 +++++++++++++++++++ .../content/docs/manual-installation.mdx | 1 + 8 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/content/docs/how-to/zero-sync.mdx diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts index 4470f53bf..f1f0bad72 100644 --- a/surfsense_web/app/sitemap.ts +++ b/surfsense_web/app/sitemap.ts @@ -212,6 +212,12 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.8, }, // How-to documentation + { + url: "https://www.surfsense.com/docs/how-to/zero-sync", + lastModified, + changeFrequency: "daily", + priority: 0.8, + }, { url: "https://www.surfsense.com/docs/how-to/realtime-collaboration", lastModified, diff --git a/surfsense_web/content/docs/docker-installation/dev-compose.mdx b/surfsense_web/content/docs/docker-installation/dev-compose.mdx index 5c800e450..599e9beb2 100644 --- a/surfsense_web/content/docs/docker-installation/dev-compose.mdx +++ b/surfsense_web/content/docs/docker-installation/dev-compose.mdx @@ -24,6 +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` | 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 bf0ca762f..1560d3759 100644 --- a/surfsense_web/content/docs/docker-installation/docker-compose.mdx +++ b/surfsense_web/content/docs/docker-installation/docker-compose.mdx @@ -48,6 +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` | +| `ZERO_CACHE_PORT` | Zero-cache real-time sync port | `5929` | ### Custom Domain / Reverse Proxy @@ -58,6 +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_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 @@ -136,6 +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) | +| `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`. @@ -169,4 +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`. +- **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 f3f932c4c..50ccc7288 100644 --- a/surfsense_web/content/docs/docker-installation/install-script.mdx +++ b/surfsense_web/content/docs/docker-installation/install-script.mdx @@ -38,3 +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) +- **Zero-cache**: [http://localhost:5929](http://localhost:5929) diff --git a/surfsense_web/content/docs/how-to/index.mdx b/surfsense_web/content/docs/how-to/index.mdx index a6571036d..be8b6f8cd 100644 --- a/surfsense_web/content/docs/how-to/index.mdx +++ b/surfsense_web/content/docs/how-to/index.mdx @@ -8,6 +8,11 @@ import { Card, Cards } from 'fumadocs-ui/components/card'; 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. + +## 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 77dc6e45b..1577b8d8b 100644 --- a/surfsense_web/content/docs/manual-installation.mdx +++ b/surfsense_web/content/docs/manual-installation.mdx @@ -408,6 +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_ZERO_CACHE_URL | URL for Zero-cache real-time sync service (e.g., `http://localhost:4848`) | ### 2. Install Dependencies From b27061e44a194e60fc112b09b87ea6e7841488c2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 18:56:32 +0200 Subject: [PATCH 10/31] feat: rewrite use-chat-session-state hook from Electric to Zero Replace @electric-sql/react useShape with @rocicorp/zero/react useQuery. Same Jotai atom update, same logic, same consumer contract. --- surfsense_web/hooks/use-chat-session-state.ts | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/surfsense_web/hooks/use-chat-session-state.ts b/surfsense_web/hooks/use-chat-session-state.ts index f3bdd7722..3c32c3d2a 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 { 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"; +import { useQuery } from "@rocicorp/zero/react"; /** - * 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]); } From 5ad25d10ea065dcd8cb856a074741801ada5ec7c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:00:20 +0200 Subject: [PATCH 11/31] feat: rewrite use-messages-electric hook from Electric to Zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace PGlite sync+live query with Zero useQuery. Maps camelCase output to snake_case to preserve consumer contract. 162 → 38 lines. --- surfsense_web/hooks/use-messages-electric.ts | 156 ++----------------- 1 file changed, 16 insertions(+), 140 deletions(-) diff --git a/surfsense_web/hooks/use-messages-electric.ts b/surfsense_web/hooks/use-messages-electric.ts index 728503de9..151c3ae2a 100644 --- a/surfsense_web/hooks/use-messages-electric.ts +++ b/surfsense_web/hooks/use-messages-electric.ts @@ -1,162 +1,38 @@ "use client"; -import { useCallback, useEffect, useRef } from "react"; +import { 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"; +import { queries } from "@/zero/queries"; +import { useQuery } from "@rocicorp/zero/react"; /** - * Syncs chat messages for a thread via Electric SQL. + * Syncs chat messages for a thread via Zero. * 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); - }, []); + const [messages] = useQuery(queries.messages.byThread({ threadId: threadId ?? -1 })); useEffect(() => { - if (!threadId || !electricClient) { - return; - } + if (!threadId || !messages) return; - const syncKey = `messages_${threadId}`; - if (syncKeyRef.current === syncKey) { - 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: String(msg.createdAt), + })); - 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 { - // Timeout - } - } - - if (!mounted) { - handle.unsubscribe(); - return; - } - - syncHandleRef.current = handle; - await fetchMessages(); - await setupLiveQuery(); - } catch { - // Sync failed - } - } - - 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 { - // Query failed - } - } - - 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 { - // Live query failed - } - } - - 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]); + onMessagesUpdateRef.current(mapped); + }, [threadId, messages]); } From c60d2f071c6a4ed1ff48c6ec628a74f6f390dc6a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:04:51 +0200 Subject: [PATCH 12/31] feat: rewrite use-connectors-electric hook from Electric to Zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace PGlite sync+live query with Zero useQuery. Maps camelCase to snake_case for SearchSourceConnector type. 217 → 44 lines. --- .../hooks/use-connectors-electric.ts | 233 +++--------------- 1 file changed, 31 insertions(+), 202 deletions(-) diff --git a/surfsense_web/hooks/use-connectors-electric.ts b/surfsense_web/hooks/use-connectors-electric.ts index 951f1d15a..3714e4af0 100644 --- a/surfsense_web/hooks/use-connectors-electric.ts +++ b/surfsense_web/hooks/use-connectors-electric.ts @@ -1,216 +1,45 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useMemo } from "react"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import type { SyncHandle } from "@/lib/electric/client"; -import { useElectricClient } from "@/lib/electric/context"; +import { queries } from "@/zero/queries"; +import { useQuery } from "@rocicorp/zero/react"; /** - * 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 + * Syncs connectors for a search space via Zero. + * Returns connectors, loading state, error, and a refresh function. */ export function useConnectorsElectric(searchSpaceId: number | string | null) { - // Get Electric client from context - ElectricProvider handles initialization - const electricClient = useElectricClient(); + const spaceId = searchSpaceId ? Number(searchSpaceId) : -1; - 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); + const [data, result] = useQuery(queries.connectors.bySpace({ searchSpaceId: spaceId })); - // 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(), - }; - } + 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]); - // 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; - } + const loading = !searchSpaceId ? false : result.type !== "complete"; + const error = !searchSpaceId ? null : null; - // 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 { - 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"], - }); - - 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]); + const refreshConnectors = async () => { + // Zero handles reactivity automatically — no manual refresh needed + }; return { connectors, loading, error, refreshConnectors }; } From 5dd101b20338396341e8054b250ce649160dfd18 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:06:32 +0200 Subject: [PATCH 13/31] feat: rewrite use-documents-processing hook from Electric to Zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace PGlite aggregate SQL (SUM/CASE) with Zero useQuery + client-side status counting. State machine (idle/processing/success/error) preserved exactly. 167 → 79 lines. --- .../hooks/use-documents-processing.ts | 177 +++++------------- 1 file changed, 46 insertions(+), 131 deletions(-) diff --git a/surfsense_web/hooks/use-documents-processing.ts b/surfsense_web/hooks/use-documents-processing.ts index bb9901e64..7075e9dae 100644 --- a/surfsense_web/hooks/use-documents-processing.ts +++ b/surfsense_web/hooks/use-documents-processing.ts @@ -1,7 +1,8 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { useElectricClient } from "@/lib/electric/context"; +import { queries } from "@/zero/queries"; +import { useQuery } from "@rocicorp/zero/react"; export type DocumentsProcessingStatus = "idle" | "processing" | "success" | "error"; @@ -15,152 +16,66 @@ 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; } From cd9d8ca991205d3de87f5d50a662a25491d84a47 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:08:17 +0200 Subject: [PATCH 14/31] feat: rewrite use-comments-electric hook from Electric to Zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace PGlite sync+live query+stream subscriber with Zero useQuery. All transformation logic preserved exactly: nested comments, mention rendering, permissions, React Query cache writes. 414 → 207 lines. --- surfsense_web/hooks/use-comments-electric.ts | 237 ++----------------- 1 file changed, 18 insertions(+), 219 deletions(-) diff --git a/surfsense_web/hooks/use-comments-electric.ts b/surfsense_web/hooks/use-comments-electric.ts index 6ca7748b5..d588504c9 100644 --- a/surfsense_web/hooks/use-comments-electric.ts +++ b/surfsense_web/hooks/use-comments-electric.ts @@ -7,14 +7,10 @@ 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"; +import { queries } from "@/zero/queries"; +import { useQuery } from "@rocicorp/zero/react"; -// 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; @@ -26,14 +22,10 @@ interface RawCommentRow { 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); @@ -44,9 +36,6 @@ function renderMentions(content: string, memberMap: Map): st }); } -/** - * Build member lookup map from membersData - */ function buildMemberMap(membersData: Membership[] | undefined): Map { const map = new Map(); if (membersData) { @@ -61,9 +50,6 @@ function buildMemberMap(membersData: Membership[] | undefined): Map): Author | null { if (!authorId) return null; const m = memberMap.get(authorId); @@ -76,20 +62,12 @@ function buildAuthor(authorId: string | null, memberMap: Map }; } -/** - * 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, @@ -109,16 +87,12 @@ function transformReply( }; } -/** - * 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 } @@ -140,7 +114,6 @@ function transformComments( } } - // Transform to Comment objects grouped by message_id const result = new Map(); for (const [messageId, group] of byMessage) { @@ -165,7 +138,6 @@ function transformComments( }; }); - // Sort by created_at comments.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); result.set(messageId, comments); } @@ -174,15 +146,12 @@ function transformComments( } /** - * Hook for syncing comments with Electric SQL real-time sync. + * 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. - * - * @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); @@ -193,13 +162,11 @@ export function useCommentsElectric(threadId: number | null) { 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; @@ -207,12 +174,6 @@ export function useCommentsElectric(threadId: number | null) { 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, @@ -230,184 +191,22 @@ export function useCommentsElectric(threadId: number | null) { } }, []); + const [data] = useQuery(queries.comments.byThread({ threadId: threadId ?? -1 })); + useEffect(() => { - if (!threadId || !electricClient) { - return; - } + if (!threadId || !data) return; - const syncKey = `comments_${threadId}`; - if (syncKeyRef.current === syncKey) { - 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: String(c.createdAt), + updated_at: String(c.updatedAt), + })); - // 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]); + updateReactQueryCache(rows); + }, [threadId, data, updateReactQueryCache]); } From 833fbb2a7695d5c48a81cb8d3e7f59823e0eefc2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:16:38 +0200 Subject: [PATCH 15/31] feat: rewrite use-documents hook from Electric to Zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep all API calls (initial load, pagination, type counts, user enrichment). Replace Electric sync+PGlite with Zero useQuery for real-time deltas. Remove baseline dedup. 511 → 361 lines. --- surfsense_web/hooks/use-documents.ts | 338 ++++++++------------------- 1 file changed, 94 insertions(+), 244 deletions(-) diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 5fee85d01..41461a3b2 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -3,25 +3,14 @@ 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"; +import { useQuery } from "@rocicorp/zero/react"; 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,104 @@ 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: String(doc.createdAt), + 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 +289,6 @@ export function useDocuments( setHasMore(false); apiLoadedCountRef.current = 0; initialLoadDoneRef.current = false; - electricBaselineIdsRef.current = null; - newestApiTimestampRef.current = null; userCacheRef.current.clear(); emailCacheRef.current.clear(); } From a02bc54e400ded43793659509454843da6f49cd5 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:21:20 +0200 Subject: [PATCH 16/31] feat: rewrite use-inbox and use-documents hooks from Electric to Zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use-inbox: Keep all API calls (initial load, pagination, unread counts, markAsRead/markAllAsRead optimistic updates). Replace Electric sync+PGlite with Zero useQuery. Category filtering in JS. 458 → 273 lines. use-documents: Keep all API calls (initial load, pagination, type counts, user enrichment). Replace Electric sync+PGlite with Zero useQuery. Remove baseline dedup. 511 → 361 lines. All 7 Electric hooks are now rewritten to use Zero. --- surfsense_web/hooks/use-inbox.ts | 323 +++++++++---------------------- 1 file changed, 89 insertions(+), 234 deletions(-) diff --git a/surfsense_web/hooks/use-inbox.ts b/surfsense_web/hooks/use-inbox.ts index f301dc90e..d15a47a39 100644 --- a/surfsense_web/hooks/use-inbox.ts +++ b/surfsense_web/hooks/use-inbox.ts @@ -3,8 +3,8 @@ 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"; +import { useQuery } from "@rocicorp/zero/react"; 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,87 @@ 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: String(item.createdAt), + updated_at: item.updatedAt ? String(item.updatedAt) : undefined, + } 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 () => { From f04ab8941832c22f38a9a55dbdd840b024620e54 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:29:08 +0200 Subject: [PATCH 17/31] refactor: rename Electric hooks and clean consumer components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename hooks to remove Electric branding: - use-connectors-electric → use-connectors-sync (useConnectorsSync) - use-messages-electric → use-messages-sync (useMessagesSync) - use-comments-electric → use-comments-sync (useCommentsSync) Clean all Electric/PGlite references in consumer components: connector-popup.tsx, thread.tsx, page.tsx, use-indexing-connectors.ts, use-connector-dialog.ts --- .../new-chat/[[...chat_id]]/page.tsx | 14 ++++---- .../assistant-ui/connector-popup.tsx | 35 +++++++------------ .../hooks/use-connector-dialog.ts | 2 +- .../hooks/use-indexing-connectors.ts | 8 ++--- .../components/assistant-ui/thread.tsx | 6 ++-- ...ments-electric.ts => use-comments-sync.ts} | 2 +- ...ors-electric.ts => use-connectors-sync.ts} | 6 ++-- ...sages-electric.ts => use-messages-sync.ts} | 2 +- 8 files changed, 32 insertions(+), 43 deletions(-) rename surfsense_web/hooks/{use-comments-electric.ts => use-comments-sync.ts} (99%) rename surfsense_web/hooks/{use-connectors-electric.ts => use-connectors-sync.ts} (88%) rename surfsense_web/hooks/{use-messages-electric.ts => use-messages-sync.ts} (96%) 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 62770690d..a9a6c5ec2 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 @@ -61,7 +61,7 @@ import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage"; import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory"; 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"; @@ -204,13 +204,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; @@ -224,11 +224,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; @@ -255,7 +255,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/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 4f4bf5cea..4e207eee3 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"; @@ -157,33 +157,24 @@ 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[], @@ -204,7 +195,7 @@ export const ConnectorIndicator = forwardRef( (connectors || []).map((c: SearchSourceConnector) => c.connector_type) ); @@ -282,7 +273,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 14183ec75..0b4e43eff 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 @@ -1569,7 +1569,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 8c2b57e75..ac83b86e5 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -91,7 +91,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getToolIcon } 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"; @@ -365,8 +365,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). diff --git a/surfsense_web/hooks/use-comments-electric.ts b/surfsense_web/hooks/use-comments-sync.ts similarity index 99% rename from surfsense_web/hooks/use-comments-electric.ts rename to surfsense_web/hooks/use-comments-sync.ts index d588504c9..05a6f2b46 100644 --- a/surfsense_web/hooks/use-comments-electric.ts +++ b/surfsense_web/hooks/use-comments-sync.ts @@ -151,7 +151,7 @@ function transformComments( * 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 useCommentsElectric(threadId: number | null) { +export function useCommentsSync(threadId: number | null) { const queryClient = useQueryClient(); const { data: membersData } = useAtomValue(membersAtom); diff --git a/surfsense_web/hooks/use-connectors-electric.ts b/surfsense_web/hooks/use-connectors-sync.ts similarity index 88% rename from surfsense_web/hooks/use-connectors-electric.ts rename to surfsense_web/hooks/use-connectors-sync.ts index 3714e4af0..602c06dfc 100644 --- a/surfsense_web/hooks/use-connectors-electric.ts +++ b/surfsense_web/hooks/use-connectors-sync.ts @@ -9,7 +9,7 @@ import { useQuery } from "@rocicorp/zero/react"; * Syncs connectors for a search space via Zero. * Returns connectors, loading state, error, and a refresh function. */ -export function useConnectorsElectric(searchSpaceId: number | string | null) { +export function useConnectorsSync(searchSpaceId: number | string | null) { const spaceId = searchSpaceId ? Number(searchSpaceId) : -1; const [data, result] = useQuery(queries.connectors.bySpace({ searchSpaceId: spaceId })); @@ -37,9 +37,7 @@ export function useConnectorsElectric(searchSpaceId: number | string | null) { const loading = !searchSpaceId ? false : result.type !== "complete"; const error = !searchSpaceId ? null : null; - const refreshConnectors = async () => { - // Zero handles reactivity automatically — no manual refresh needed - }; + const refreshConnectors = async () => {}; return { connectors, loading, error, refreshConnectors }; } diff --git a/surfsense_web/hooks/use-messages-electric.ts b/surfsense_web/hooks/use-messages-sync.ts similarity index 96% rename from surfsense_web/hooks/use-messages-electric.ts rename to surfsense_web/hooks/use-messages-sync.ts index 151c3ae2a..1d1fb2e25 100644 --- a/surfsense_web/hooks/use-messages-electric.ts +++ b/surfsense_web/hooks/use-messages-sync.ts @@ -9,7 +9,7 @@ import { useQuery } from "@rocicorp/zero/react"; * Syncs chat messages for a thread via Zero. * Calls onMessagesUpdate when messages change. */ -export function useMessagesElectric( +export function useMessagesSync( threadId: number | null, onMessagesUpdate: (messages: RawMessage[]) => void ) { From cf21eaacfc7f2624c2668cba214a8a42190d78db Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 19:49:28 +0200 Subject: [PATCH 18/31] fix: critical timestamp parsing and audit fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix timestamp conversion: String(epochMs) → new Date(epochMs).toISOString() in use-messages-sync, use-comments-sync, use-documents, use-inbox. Without this, date comparisons (isEdited, cutoff filters) would fail. - Fix updated_at: undefined → null in use-inbox to match InboxItem type - Fix ZeroProvider: skip Zero connection for unauthenticated users - Clean 30+ stale "Electric SQL" comments in backend Python code --- .../app/connectors/composio_gmail_connector.py | 2 +- .../composio_google_calendar_connector.py | 4 ++-- .../composio_google_drive_connector.py | 2 +- surfsense_backend/app/db.py | 4 ++-- surfsense_backend/app/routes/__init__.py | 2 +- surfsense_backend/app/routes/documents_routes.py | 2 +- .../app/routes/notifications_routes.py | 10 +++++----- .../routes/search_source_connectors_routes.py | 16 ++++++++-------- .../app/services/chat_comments_service.py | 4 ++-- .../app/services/notification_service.py | 6 +++--- .../app/tasks/celery_tasks/document_tasks.py | 4 ++-- .../tasks/connector_indexers/airtable_indexer.py | 4 ++-- .../connector_indexers/bookstack_indexer.py | 2 +- .../tasks/connector_indexers/clickup_indexer.py | 2 +- .../connector_indexers/confluence_indexer.py | 2 +- .../tasks/connector_indexers/discord_indexer.py | 2 +- .../connector_indexers/elasticsearch_indexer.py | 2 +- .../tasks/connector_indexers/github_indexer.py | 2 +- .../google_calendar_indexer.py | 2 +- .../connector_indexers/google_gmail_indexer.py | 2 +- .../app/tasks/connector_indexers/jira_indexer.py | 2 +- .../tasks/connector_indexers/linear_indexer.py | 2 +- .../app/tasks/connector_indexers/luma_indexer.py | 2 +- .../tasks/connector_indexers/notion_indexer.py | 4 ++-- .../tasks/connector_indexers/obsidian_indexer.py | 2 +- .../tasks/connector_indexers/slack_indexer.py | 4 ++-- .../tasks/connector_indexers/teams_indexer.py | 4 ++-- .../connector_indexers/webcrawler_indexer.py | 2 +- .../components/providers/ZeroProvider.tsx | 9 +++++++-- surfsense_web/hooks/use-comments-sync.ts | 4 ++-- surfsense_web/hooks/use-documents.ts | 2 +- surfsense_web/hooks/use-inbox.ts | 4 ++-- surfsense_web/hooks/use-messages-sync.ts | 2 +- 33 files changed, 62 insertions(+), 57 deletions(-) diff --git a/surfsense_backend/app/connectors/composio_gmail_connector.py b/surfsense_backend/app/connectors/composio_gmail_connector.py index e675085db..b9b2e7925 100644 --- a/surfsense_backend/app/connectors/composio_gmail_connector.py +++ b/surfsense_backend/app/connectors/composio_gmail_connector.py @@ -664,7 +664,7 @@ async def index_composio_gmail( on_heartbeat_callback=on_heartbeat_callback, ) - # CRITICAL: Always update timestamp so Electric SQL syncs + # CRITICAL: Always update timestamp so Zero syncs await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit to ensure all documents are persisted diff --git a/surfsense_backend/app/connectors/composio_google_calendar_connector.py b/surfsense_backend/app/connectors/composio_google_calendar_connector.py index 6344f9f38..a0b76e5d7 100644 --- a/surfsense_backend/app/connectors/composio_google_calendar_connector.py +++ b/surfsense_backend/app/connectors/composio_google_calendar_connector.py @@ -255,7 +255,7 @@ async def index_composio_google_calendar( await task_logger.log_task_success( log_entry, success_msg, {"events_count": 0} ) - # CRITICAL: Update timestamp even when no events found so Electric SQL syncs and UI shows indexed status + # CRITICAL: Update timestamp even when no events found so Zero syncs and UI shows indexed status await update_connector_last_indexed(session, connector, update_last_indexed) await session.commit() return ( @@ -503,7 +503,7 @@ async def index_composio_google_calendar( 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/connectors/composio_google_drive_connector.py b/surfsense_backend/app/connectors/composio_google_drive_connector.py index 30ce4a77b..3f23c9667 100644 --- a/surfsense_backend/app/connectors/composio_google_drive_connector.py +++ b/surfsense_backend/app/connectors/composio_google_drive_connector.py @@ -775,7 +775,7 @@ async def index_composio_google_drive( flag_modified(connector, "config") logger.info(f"Saved indexing settings hash for connector {connector_id}") - # CRITICAL: Always update timestamp so Electric SQL syncs and UI shows indexed status + # CRITICAL: Always update timestamp so Zero syncs and UI shows indexed status await update_connector_last_indexed(session, connector, update_last_indexed) # Final commit diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 2ce48c16d..5cd1d21b5 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -712,7 +712,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"), @@ -782,7 +782,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 b241aa2fb..509dca4bc 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -1433,7 +1433,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: @@ -1460,7 +1460,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: @@ -1486,7 +1486,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 @@ -1509,7 +1509,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}") @@ -1525,13 +1525,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 @@ -1547,7 +1547,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 @@ -2650,7 +2650,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 24e822060..9dddca063 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py @@ -554,7 +554,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 6e2408cbd..009d203eb 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py @@ -477,7 +477,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_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index 5e20e9cda..1a0a2f937 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -10,8 +10,13 @@ const cacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:484 export function ZeroProvider({ children }: { children: React.ReactNode }) { const { data: user } = useAtomValue(currentUserAtom); - const userID = user?.id ? String(user.id) : ""; - const context = user?.id ? { userId: String(user.id) } : undefined; + + if (!user?.id) { + return <>{children}; + } + + const userID = String(user.id); + const context = { userId: userID }; return ( diff --git a/surfsense_web/hooks/use-comments-sync.ts b/surfsense_web/hooks/use-comments-sync.ts index 05a6f2b46..48d97d0e6 100644 --- a/surfsense_web/hooks/use-comments-sync.ts +++ b/surfsense_web/hooks/use-comments-sync.ts @@ -203,8 +203,8 @@ export function useCommentsSync(threadId: number | null) { parent_id: c.parentId ?? null, author_id: c.authorId ?? null, content: c.content, - created_at: String(c.createdAt), - updated_at: String(c.updatedAt), + created_at: new Date(c.createdAt).toISOString(), + updated_at: new Date(c.updatedAt).toISOString(), })); updateReactQueryCache(rows); diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 41461a3b2..dbf99b349 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -242,7 +242,7 @@ export function useDocuments( created_by_email: doc.createdById ? (emailCacheRef.current.get(doc.createdById) ?? null) : null, - created_at: String(doc.createdAt), + created_at: new Date(doc.createdAt).toISOString(), status: (doc.status as unknown as DocumentStatusType) ?? { state: "ready" }, })); diff --git a/surfsense_web/hooks/use-inbox.ts b/surfsense_web/hooks/use-inbox.ts index d15a47a39..196a97b4e 100644 --- a/surfsense_web/hooks/use-inbox.ts +++ b/surfsense_web/hooks/use-inbox.ts @@ -154,8 +154,8 @@ export function useInbox( message: item.message, read: item.read, metadata: item.metadata as unknown as Record, - created_at: String(item.createdAt), - updated_at: item.updatedAt ? String(item.updatedAt) : undefined, + created_at: new Date(item.createdAt).toISOString(), + updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : null, } as InboxItem)); let updated = prev.map((existing) => { diff --git a/surfsense_web/hooks/use-messages-sync.ts b/surfsense_web/hooks/use-messages-sync.ts index 1d1fb2e25..da2869bc6 100644 --- a/surfsense_web/hooks/use-messages-sync.ts +++ b/surfsense_web/hooks/use-messages-sync.ts @@ -30,7 +30,7 @@ export function useMessagesSync( role: msg.role, content: msg.content, author_id: msg.authorId ?? null, - created_at: String(msg.createdAt), + created_at: new Date(msg.createdAt).toISOString(), })); onMessagesUpdateRef.current(mapped); From 7d7d032e78d73deb19ece419f389e6f6a5b28fa0 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 20:01:06 +0200 Subject: [PATCH 19/31] docs: add database permission requirements for Zero in managed environments Document that the Postgres user needs REPLICATION and CREATE privileges for zero-cache. Docker setup handles this automatically (superuser), but managed databases (RDS, Supabase, etc.) may need explicit grants. --- .../content/docs/how-to/zero-sync.mdx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/surfsense_web/content/docs/how-to/zero-sync.mdx b/surfsense_web/content/docs/how-to/zero-sync.mdx index 4e85af19e..65c57f76f 100644 --- a/surfsense_web/content/docs/how-to/zero-sync.mdx +++ b/surfsense_web/content/docs/how-to/zero-sync.mdx @@ -48,6 +48,24 @@ If running the frontend outside Docker (e.g., `pnpm dev`), you need: 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: From 29b9cc814f5d39bb0d2bd03a1ebef942b070e2fd Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 20:22:48 +0200 Subject: [PATCH 20/31] fix: make migration 104 idempotent with if_not_exists on index creation The notification composite indexes may already exist from the model's __table_args__, causing DuplicateTableError on fresh migrations. --- .../versions/104_add_notification_composite_indexes.py | 4 ++++ 1 file changed, 4 insertions(+) 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, ) From 4efb4fab4b8f3fb0ad8918a56b6650d1bf5d6578 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 20:27:09 +0200 Subject: [PATCH 21/31] fix: make migration 107 idempotent for video_presentations table Check information_schema before creating table to avoid DuplicateTableError when table already exists from model metadata. Original op.create_table preserved exactly, only skipped if exists. --- .../107_add_video_presentations_table.py | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) 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, ) From 8dd539c38452d3386e1b6d92b15a0345c5f02df7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 23 Mar 2026 20:58:42 +0200 Subject: [PATCH 22/31] feat: add Zero query endpoint and configure ZERO_QUERY_URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create app/api/zero/query/route.ts — resolves named queries to ZQL using handleQueryRequest and mustGetQuery from @rocicorp/zero - Add ZERO_QUERY_URL to both docker-compose files: - dev: http://host.docker.internal:3000 (reaches local Next.js from Docker) - prod: http://frontend:3000 (Docker service networking) Without this endpoint, zero-cache cannot resolve named queries and no data syncs to the client. --- docker/docker-compose.dev.yml | 1 + docker/docker-compose.yml | 1 + surfsense_web/app/api/zero/query/route.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 surfsense_web/app/api/zero/query/route.ts diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index b91b95af7..233a1629a 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -182,6 +182,7 @@ services: - 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://host.docker.internal:3000/api/zero/query} volumes: - zero_cache_data:/data restart: unless-stopped diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d659fbd15..9dc5d5e14 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -168,6 +168,7 @@ services: 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} volumes: - zero_cache_data:/data restart: unless-stopped 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..baee8bd75 --- /dev/null +++ b/surfsense_web/app/api/zero/query/route.ts @@ -0,0 +1,18 @@ +import { mustGetQuery } from "@rocicorp/zero"; +import { handleQueryRequest } from "@rocicorp/zero/server"; +import { NextResponse } from "next/server"; +import { queries } from "@/zero/queries"; +import { schema } from "@/zero/schema"; + +export async function POST(request: Request) { + const result = await handleQueryRequest( + (name, args) => { + const query = mustGetQuery(queries, name); + return query.fn({ args, ctx: undefined }); + }, + schema, + request, + ); + + return NextResponse.json(result); +} From cab33d9f4c8c422feabba9e1e3cd916a501a6ed9 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 15:06:58 +0200 Subject: [PATCH 23/31] fix: make ZERO_QUERY_URL portable across all environments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default to Docker service networking (http://frontend:3000) in both compose files — works when all services run in Docker - Add extra_hosts (host.docker.internal:host-gateway) for cross-platform host access when running frontend outside Docker - Document override examples in .env.example for local dev and custom domain setups --- docker/.env.example | 8 ++++++++ docker/docker-compose.dev.yml | 4 +++- docker/docker-compose.yml | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index 04e67de04..716405f8e 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -85,6 +85,14 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # ZERO_CVR_DB=postgresql://surfsense:surfsense@db:5432/surfsense # ZERO_CHANGE_DB=postgresql://surfsense:surfsense@db:5432/surfsense +# URL where zero-cache sends queries for resolution (server-to-server). +# Default: http://frontend:3000/api/zero/query (Docker service networking). +# Override when running the frontend outside Docker: +# ZERO_QUERY_URL=http://host.docker.internal:3000/api/zero/query +# Override for custom domain: +# ZERO_QUERY_URL=https://app.yourdomain.com/api/zero/query +# ZERO_QUERY_URL=http://frontend:3000/api/zero/query + # ------------------------------------------------------------------------------ # Database (defaults work out of the box, change for security) # ------------------------------------------------------------------------------ diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 233a1629a..e6cb3fd1d 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -173,6 +173,8 @@ services: image: rocicorp/zero:0.26.2 ports: - "${ZERO_CACHE_PORT:-4848}:4848" + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: db: condition: service_healthy @@ -182,7 +184,7 @@ services: - 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://host.docker.internal:3000/api/zero/query} + - ZERO_QUERY_URL=${ZERO_QUERY_URL:-http://frontend:3000/api/zero/query} volumes: - zero_cache_data:/data restart: unless-stopped diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9dc5d5e14..9b9993c7f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -162,6 +162,8 @@ services: image: rocicorp/zero:0.26.2 ports: - "${ZERO_CACHE_PORT:-5929}:4848" + extra_hosts: + - "host.docker.internal:host-gateway" environment: 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}} From a74aa4da4fd5a7a42e7b7491291748a8f954d591 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 16:06:20 +0200 Subject: [PATCH 24/31] add auth to Zero query endpoint and ZeroProvider --- surfsense_web/app/api/zero/query/route.ts | 36 ++++++++++++++- .../components/providers/ZeroProvider.tsx | 44 ++++++++++++++++--- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/surfsense_web/app/api/zero/query/route.ts b/surfsense_web/app/api/zero/query/route.ts index baee8bd75..98cd4551c 100644 --- a/surfsense_web/app/api/zero/query/route.ts +++ b/surfsense_web/app/api/zero/query/route.ts @@ -1,17 +1,49 @@ 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 { error: NextResponse.json({ error: "Unauthorized" }, { status: 401 }) }; + } + + 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: undefined }); + return query.fn({ args, ctx: auth.ctx }); }, schema, - request, + request ); return NextResponse.json(result); diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index 1a0a2f937..3bd579ea9 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -1,26 +1,60 @@ "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"; -import { ZeroProvider as ZeroReactProvider } from "@rocicorp/zero/react"; -import { useAtomValue } from "jotai"; 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 token = getBearerToken(); - if (!user?.id) { + if (!user?.id || !token) { return <>{children}; } const userID = String(user.id); const context = { userId: userID }; + const auth = token; return ( - - {children} + + {children} ); } From 0e49cc33f8d9b8cb55184e4c78e75decd070c40c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 16:07:28 +0200 Subject: [PATCH 25/31] format: auto-fix via pnpm format:fix --- .../components/DocumentsTableShell.tsx | 20 +++---- .../new-chat/[[...chat_id]]/page.tsx | 2 +- .../[search_space_id]/team/team-content.tsx | 2 +- surfsense_web/app/global-error.tsx | 2 +- surfsense_web/app/layout.tsx | 12 ++-- .../assistant-ui/connector-popup.tsx | 3 +- .../components/assistant-ui/thread.tsx | 8 ++- .../layout/providers/LayoutDataProvider.tsx | 11 ++-- .../ui/sidebar/AllPrivateChatsSidebar.tsx | 2 +- .../ui/sidebar/AllSharedChatsSidebar.tsx | 2 +- .../public-chat/public-chat-view.tsx | 2 +- .../settings/image-model-manager.tsx | 4 +- .../settings/model-config-manager.tsx | 2 +- .../components/settings/team-dialog.tsx | 2 +- .../settings/user-settings-dialog.tsx | 2 +- surfsense_web/components/tool-ui/index.ts | 2 +- .../video-presentation/combined-player.tsx | 13 ++-- .../generate-video-presentation.tsx | 59 ++++++------------- surfsense_web/components/ui/hero-carousel.tsx | 7 +-- surfsense_web/docker-entrypoint.js | 5 +- surfsense_web/hooks/use-chat-session-state.ts | 2 +- surfsense_web/hooks/use-comments-sync.ts | 2 +- surfsense_web/hooks/use-connectors-sync.ts | 2 +- .../hooks/use-documents-processing.ts | 6 +- surfsense_web/hooks/use-documents.ts | 6 +- surfsense_web/hooks/use-inbox.ts | 42 ++++++------- surfsense_web/hooks/use-messages-sync.ts | 2 +- surfsense_web/lib/remotion/compile-check.ts | 30 ++++------ surfsense_web/lib/remotion/dom-to-pptx.d.ts | 2 +- surfsense_web/next.config.ts | 2 +- surfsense_web/zero/queries/chat.ts | 17 ++---- surfsense_web/zero/queries/documents.ts | 14 ++--- surfsense_web/zero/queries/inbox.ts | 6 +- surfsense_web/zero/schema/chat.ts | 2 +- surfsense_web/zero/schema/documents.ts | 2 +- surfsense_web/zero/schema/inbox.ts | 2 +- surfsense_web/zero/schema/index.ts | 2 +- 37 files changed, 128 insertions(+), 175 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index cc9881d3b..e2309299d 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAtomValue, useSetAtom } from "jotai"; import { AlertCircle, CheckCircle2, @@ -16,12 +17,11 @@ import { Trash2, User, } from "lucide-react"; -import { useAtomValue, useSetAtom } from "jotai"; import { useTranslations } from "next-intl"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { membersAtom } from "@/atoms/members/members-query.atoms"; -import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { toast } from "sonner"; +import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; +import { membersAtom } from "@/atoms/members/members-query.atoms"; import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; import { MarkdownViewer } from "@/components/markdown-viewer"; @@ -35,14 +35,9 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Drawer, @@ -51,7 +46,12 @@ import { DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { 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 a9a6c5ec2..7c9f303e7 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 @@ -40,7 +40,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 { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI, @@ -59,6 +58,7 @@ 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 { useMessagesSync } from "@/hooks/use-messages-sync"; diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx index 3e1ab0af1..43b4d0b7a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx @@ -20,7 +20,6 @@ import { UserPlus, Users, } from "lucide-react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { @@ -44,6 +43,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { diff --git a/surfsense_web/app/global-error.tsx b/surfsense_web/app/global-error.tsx index ec558052d..8e6f1e445 100644 --- a/surfsense_web/app/global-error.tsx +++ b/surfsense_web/app/global-error.tsx @@ -1,7 +1,7 @@ "use client"; -import posthog from "posthog-js"; import NextError from "next/error"; +import posthog from "posthog-js"; import { useEffect } from "react"; export default function GlobalError({ diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 9ba06c04d..784fd3bcf 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -4,9 +4,9 @@ import { RootProvider } from "fumadocs-ui/provider/next"; import { Roboto } from "next/font/google"; import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider"; import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider"; -import { ZeroProvider } from "@/components/providers/ZeroProvider"; 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"; @@ -140,11 +140,11 @@ export default function RootLayout({ defaultTheme="system" > - - - {children} - - + + + {children} + + diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 4e207eee3..dc74e6b57 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -164,8 +164,7 @@ export const ConnectorIndicator = forwardRef 0 || (connectorsLoading && !connectorsError); + const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError); const connectors = useSyncData ? connectorsFromSync : allConnectors || []; const refreshConnectors = async () => { diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index ac83b86e5..e0b39f6e4 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -938,7 +938,13 @@ const TOOL_GROUPS: { label: string; tools: string[] }[] = [ }, { 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 47a2b2362..ec58ef68c 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -3,7 +3,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react"; -import { Spinner } from "@/components/ui/spinner"; import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; @@ -22,6 +21,10 @@ import { userSettingsDialogAtom, } from "@/atoms/settings/settings-dialog.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { MorePagesDialog } from "@/components/settings/more-pages-dialog"; +import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog"; +import { TeamDialog } from "@/components/settings/team-dialog"; +import { UserSettingsDialog } from "@/components/settings/user-settings-dialog"; import { AlertDialog, AlertDialogAction, @@ -42,7 +45,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; - +import { Spinner } from "@/components/ui/spinner"; import { useAnnouncements } from "@/hooks/use-announcements"; import { useDocumentsProcessing } from "@/hooks/use-documents-processing"; import { useInbox } from "@/hooks/use-inbox"; @@ -53,10 +56,6 @@ import { logout } from "@/lib/auth-utils"; import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence"; import { resetUser, trackLogout } from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; -import { MorePagesDialog } from "@/components/settings/more-pages-dialog"; -import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog"; -import { TeamDialog } from "@/components/settings/team-dialog"; -import { UserSettingsDialog } from "@/components/settings/user-settings-dialog"; import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types"; import { CreateSearchSpaceDialog } from "../ui/dialogs"; import { LayoutShell } from "../ui/shell"; diff --git a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx index eb75842ba..1e857ec97 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx @@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -36,7 +37,6 @@ import { import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useLongPress } from "@/hooks/use-long-press"; diff --git a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx index 81c75f9d7..01fa1714f 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllSharedChatsSidebar.tsx @@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -36,7 +37,6 @@ import { import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useLongPress } from "@/hooks/use-long-press"; diff --git a/surfsense_web/components/public-chat/public-chat-view.tsx b/surfsense_web/components/public-chat/public-chat-view.tsx index 1407087e2..4adaa83e5 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/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx index 0b28c9a7f..09c8bb669 100644 --- a/surfsense_web/components/settings/image-model-manager.tsx +++ b/surfsense_web/components/settings/image-model-manager.tsx @@ -15,7 +15,6 @@ import { } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { createImageGenConfigMutationAtom, deleteImageGenConfigMutationAtom, @@ -38,6 +37,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { @@ -69,12 +69,12 @@ import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { useMediaQuery } from "@/hooks/use-media-query"; import { getImageGenModelsByProvider, IMAGE_GEN_PROVIDERS, } from "@/contracts/enums/image-gen-providers"; import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types"; +import { useMediaQuery } from "@/hooks/use-media-query"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index f4a584942..80bfd8e31 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -13,7 +13,6 @@ import { Wand2, } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { createNewLLMConfigMutationAtom, @@ -36,6 +35,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; diff --git a/surfsense_web/components/settings/team-dialog.tsx b/surfsense_web/components/settings/team-dialog.tsx index 681964afc..a3a962378 100644 --- a/surfsense_web/components/settings/team-dialog.tsx +++ b/surfsense_web/components/settings/team-dialog.tsx @@ -2,9 +2,9 @@ import { useAtom } from "jotai"; import { useTranslations } from "next-intl"; +import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content"; import { teamDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; -import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content"; interface TeamDialogProps { searchSpaceId: number; diff --git a/surfsense_web/components/settings/user-settings-dialog.tsx b/surfsense_web/components/settings/user-settings-dialog.tsx index 426903a23..855f12b61 100644 --- a/surfsense_web/components/settings/user-settings-dialog.tsx +++ b/surfsense_web/components/settings/user-settings-dialog.tsx @@ -3,9 +3,9 @@ import { useAtom } from "jotai"; import { KeyRound, User } from "lucide-react"; import { useTranslations } from "next-intl"; -import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { ApiKeyContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent"; import { ProfileContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ProfileContent"; +import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { SettingsDialog } from "@/components/settings/settings-dialog"; export function UserSettingsDialog() { 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 adf7126b8..da704a42a 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", }, { @@ -176,9 +175,7 @@ function HeroCarousel() { const id = setTimeout(() => { directionRef.current = "forward"; - setActiveIndex((prev) => - prev >= carouselItems.length - 1 ? 0 : prev + 1 - ); + setActiveIndex((prev) => (prev >= carouselItems.length - 1 ? 0 : prev + 1)); }, AUTOPLAY_MS); return () => clearTimeout(id); diff --git a/surfsense_web/docker-entrypoint.js b/surfsense_web/docker-entrypoint.js index dad52b73e..8323f5652 100644 --- a/surfsense_web/docker-entrypoint.js +++ b/surfsense_web/docker-entrypoint.js @@ -22,7 +22,10 @@ const replacements = [ 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_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"], ]; diff --git a/surfsense_web/hooks/use-chat-session-state.ts b/surfsense_web/hooks/use-chat-session-state.ts index 3c32c3d2a..467e360aa 100644 --- a/surfsense_web/hooks/use-chat-session-state.ts +++ b/surfsense_web/hooks/use-chat-session-state.ts @@ -1,10 +1,10 @@ "use client"; +import { useQuery } from "@rocicorp/zero/react"; import { useSetAtom } from "jotai"; import { useEffect } from "react"; import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; import { queries } from "@/zero/queries"; -import { useQuery } from "@rocicorp/zero/react"; /** * Syncs chat session state for a thread via Zero. diff --git a/surfsense_web/hooks/use-comments-sync.ts b/surfsense_web/hooks/use-comments-sync.ts index 48d97d0e6..b6a68364d 100644 --- a/surfsense_web/hooks/use-comments-sync.ts +++ b/surfsense_web/hooks/use-comments-sync.ts @@ -1,5 +1,6 @@ "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"; @@ -9,7 +10,6 @@ import type { Author, Comment, CommentReply } from "@/contracts/types/chat-comme import type { Membership } from "@/contracts/types/members.types"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { queries } from "@/zero/queries"; -import { useQuery } from "@rocicorp/zero/react"; interface RawCommentRow { id: number; diff --git a/surfsense_web/hooks/use-connectors-sync.ts b/surfsense_web/hooks/use-connectors-sync.ts index 602c06dfc..d36728118 100644 --- a/surfsense_web/hooks/use-connectors-sync.ts +++ b/surfsense_web/hooks/use-connectors-sync.ts @@ -1,9 +1,9 @@ "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"; -import { useQuery } from "@rocicorp/zero/react"; /** * Syncs connectors for a search space via Zero. diff --git a/surfsense_web/hooks/use-documents-processing.ts b/surfsense_web/hooks/use-documents-processing.ts index 7075e9dae..e39c03de0 100644 --- a/surfsense_web/hooks/use-documents-processing.ts +++ b/surfsense_web/hooks/use-documents-processing.ts @@ -1,8 +1,8 @@ "use client"; +import { useQuery } from "@rocicorp/zero/react"; import { useEffect, useRef, useState } from "react"; import { queries } from "@/zero/queries"; -import { useQuery } from "@rocicorp/zero/react"; export type DocumentsProcessingStatus = "idle" | "processing" | "success" | "error"; @@ -20,9 +20,7 @@ export function useDocumentsProcessing(searchSpaceId: number | null): DocumentsP const wasProcessingRef = useRef(false); const successTimerRef = useRef | null>(null); - const [documents] = useQuery( - queries.documents.bySpace({ searchSpaceId: searchSpaceId ?? -1 }) - ); + const [documents] = useQuery(queries.documents.bySpace({ searchSpaceId: searchSpaceId ?? -1 })); useEffect(() => { if (!searchSpaceId || !documents) return; diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index dbf99b349..df782ca83 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -1,10 +1,10 @@ "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 { queries } from "@/zero/queries"; -import { useQuery } from "@rocicorp/zero/react"; export interface DocumentStatusType { state: "ready" | "pending" | "processing" | "failed"; @@ -254,7 +254,9 @@ export function useDocuments( ...existing, title: liveItem.title, document_type: liveItem.documentType, - status: (liveItem.status as unknown as DocumentStatusType) ?? { state: "ready" as const }, + status: (liveItem.status as unknown as DocumentStatusType) ?? { + state: "ready" as const, + }, }; } } diff --git a/surfsense_web/hooks/use-inbox.ts b/surfsense_web/hooks/use-inbox.ts index 196a97b4e..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 { queries } from "@/zero/queries"; -import { useQuery } from "@rocicorp/zero/react"; export type { InboxItem, @@ -118,9 +118,7 @@ export function useInbox( }, [userId, searchSpaceId, category, prefetchedUnread, prefetchedUnreadReady]); // EFFECT 2: Zero real-time sync for notification updates - const [zeroNotifications] = useQuery( - queries.notifications.byUser({ userId: userId ?? "" }) - ); + const [zeroNotifications] = useQuery(queries.notifications.byUser({ userId: userId ?? "" })); useEffect(() => { if (!userId || !searchSpaceId || !zeroNotifications || !initialLoadDoneRef.current) return; @@ -134,9 +132,7 @@ export function useInbox( return true; }); - const recentItems = validItems.filter( - (item) => new Date(item.createdAt) > cutoff - ); + const recentItems = validItems.filter((item) => new Date(item.createdAt) > cutoff); const liveIds = new Set(recentItems.map((d) => d.id)); @@ -145,18 +141,21 @@ export function useInbox( 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)); + .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); @@ -187,10 +186,7 @@ export function useInbox( // 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 - ); + olderUnreadOffsetRef.current = Math.max(0, apiUnreadTotalRef.current - recentUnreadCount); } if (olderUnreadOffsetRef.current !== null) { diff --git a/surfsense_web/hooks/use-messages-sync.ts b/surfsense_web/hooks/use-messages-sync.ts index da2869bc6..ddbe8a757 100644 --- a/surfsense_web/hooks/use-messages-sync.ts +++ b/surfsense_web/hooks/use-messages-sync.ts @@ -1,9 +1,9 @@ "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"; -import { useQuery } from "@rocicorp/zero/react"; /** * Syncs chat messages for a thread via Zero. 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/next.config.ts b/surfsense_web/next.config.ts index 0f71a9eaf..f02cf4d03 100644 --- a/surfsense_web/next.config.ts +++ b/surfsense_web/next.config.ts @@ -1,7 +1,7 @@ -import path from "path"; import { createMDX } from "fumadocs-mdx/next"; import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; +import path from "path"; // Create the next-intl plugin const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); diff --git a/surfsense_web/zero/queries/chat.ts b/surfsense_web/zero/queries/chat.ts index 45ffa7ea2..de8b13f8a 100644 --- a/surfsense_web/zero/queries/chat.ts +++ b/surfsense_web/zero/queries/chat.ts @@ -3,24 +3,19 @@ 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"), + 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"), + 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(), + 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 index aa34bf718..97088945f 100644 --- a/surfsense_web/zero/queries/documents.ts +++ b/surfsense_web/zero/queries/documents.ts @@ -3,19 +3,13 @@ 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"), + 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"), + 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 index 6221ef345..d85b7212f 100644 --- a/surfsense_web/zero/queries/inbox.ts +++ b/surfsense_web/zero/queries/inbox.ts @@ -3,9 +3,7 @@ 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"), + byUser: defineQuery(z.object({ userId: z.string() }), ({ args: { userId } }) => + zql.notifications.where("userId", userId).orderBy("createdAt", "desc") ), }; diff --git a/surfsense_web/zero/schema/chat.ts b/surfsense_web/zero/schema/chat.ts index b8b7fbb93..0293059fd 100644 --- a/surfsense_web/zero/schema/chat.ts +++ b/surfsense_web/zero/schema/chat.ts @@ -1,4 +1,4 @@ -import { table, string, number, json } from "@rocicorp/zero"; +import { json, number, string, table } from "@rocicorp/zero"; export const newChatMessageTable = table("new_chat_messages") .columns({ diff --git a/surfsense_web/zero/schema/documents.ts b/surfsense_web/zero/schema/documents.ts index c50fc959b..ceeefd877 100644 --- a/surfsense_web/zero/schema/documents.ts +++ b/surfsense_web/zero/schema/documents.ts @@ -1,4 +1,4 @@ -import { table, string, number, boolean, json } from "@rocicorp/zero"; +import { boolean, json, number, string, table } from "@rocicorp/zero"; export const documentTable = table("documents") .columns({ diff --git a/surfsense_web/zero/schema/inbox.ts b/surfsense_web/zero/schema/inbox.ts index 3eca9522f..946180ba4 100644 --- a/surfsense_web/zero/schema/inbox.ts +++ b/surfsense_web/zero/schema/inbox.ts @@ -1,4 +1,4 @@ -import { table, string, number, boolean, json } from "@rocicorp/zero"; +import { boolean, json, number, string, table } from "@rocicorp/zero"; export const notificationTable = table("notifications") .columns({ diff --git a/surfsense_web/zero/schema/index.ts b/surfsense_web/zero/schema/index.ts index 1c3e89151..0a6587e92 100644 --- a/surfsense_web/zero/schema/index.ts +++ b/surfsense_web/zero/schema/index.ts @@ -1,4 +1,4 @@ -import { createSchema, createBuilder, relationships } from "@rocicorp/zero"; +import { createBuilder, createSchema, relationships } from "@rocicorp/zero"; import { chatCommentTable, chatSessionStateTable, newChatMessageTable } from "./chat"; import { documentTable, searchSourceConnectorTable } from "./documents"; import { notificationTable } from "./inbox"; From 5c98f1c71796a9e73ef39d945528be6f0bf2ab80 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 16:25:13 +0200 Subject: [PATCH 26/31] add ZERO_MUTATE_URL and no-op mutate endpoint for zero-cache auth passthrough --- docker/.env.example | 11 +++++++++-- docker/docker-compose.dev.yml | 1 + docker/docker-compose.yml | 1 + surfsense_web/app/api/zero/mutate/route.ts | 5 +++++ 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 surfsense_web/app/api/zero/mutate/route.ts diff --git a/docker/.env.example b/docker/.env.example index 716405f8e..766f92dcc 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -85,13 +85,20 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 # ZERO_CVR_DB=postgresql://surfsense:surfsense@db:5432/surfsense # ZERO_CHANGE_DB=postgresql://surfsense:surfsense@db:5432/surfsense -# URL where zero-cache sends queries for resolution (server-to-server). -# Default: http://frontend:3000/api/zero/query (Docker service networking). +# 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) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index e6cb3fd1d..564ecd772 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -185,6 +185,7 @@ services: - 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 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9b9993c7f..b03efdd2f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -171,6 +171,7 @@ services: 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 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([]); +} From 54ffdc5aad595720748c438deaebf68756075698 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 16:39:37 +0200 Subject: [PATCH 27/31] fix: always init Zero when user exists, pass auth as optional --- surfsense_web/components/providers/ZeroProvider.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index 3bd579ea9..cd35fc02d 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -42,15 +42,14 @@ function ZeroAuthGuard({ children }: { children: React.ReactNode }) { export function ZeroProvider({ children }: { children: React.ReactNode }) { const { data: user } = useAtomValue(currentUserAtom); - const token = getBearerToken(); - if (!user?.id || !token) { + if (!user?.id) { return <>{children}; } const userID = String(user.id); const context = { userId: userID }; - const auth = token; + const auth = getBearerToken() || undefined; return ( From 657f54690866aac32d5658a32bff815536e23aa5 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 16:59:42 +0200 Subject: [PATCH 28/31] fix: always render ZeroProvider, allow anon queries without 401 --- surfsense_web/app/api/zero/query/route.ts | 2 +- .../components/providers/ZeroProvider.tsx | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/surfsense_web/app/api/zero/query/route.ts b/surfsense_web/app/api/zero/query/route.ts index 98cd4551c..3d8ff0d33 100644 --- a/surfsense_web/app/api/zero/query/route.ts +++ b/surfsense_web/app/api/zero/query/route.ts @@ -12,7 +12,7 @@ async function authenticateRequest( ): Promise<{ ctx: Context; error?: never } | { ctx?: never; error: NextResponse }> { const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { - return { error: NextResponse.json({ error: "Unauthorized" }, { status: 401 }) }; + return { ctx: undefined }; } try { diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index cd35fc02d..f4df921f3 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -43,17 +43,23 @@ function ZeroAuthGuard({ children }: { children: React.ReactNode }) { export function ZeroProvider({ children }: { children: React.ReactNode }) { const { data: user } = useAtomValue(currentUserAtom); - if (!user?.id) { - return <>{children}; - } + 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 userID = String(user.id); - const context = { userId: userID }; - const auth = getBearerToken() || undefined; + const opts = { + userID, + schema, + queries, + context, + cacheURL, + auth, + }; return ( - - {children} + + {hasUser ? {children} : children} ); } From 0916c1addddef5047a9c78a27ed8ddacd37b1910 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 17:07:11 +0200 Subject: [PATCH 29/31] remove stale ElectricSQL references from changelog and test fixtures --- surfsense_backend/tests/fixtures/sample.md | 2 +- surfsense_web/changelog/content/2026-01-26.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/changelog/content/2026-01-26.mdx b/surfsense_web/changelog/content/2026-01-26.mdx index e2092bc83..d3bbca6c8 100644 --- a/surfsense_web/changelog/content/2026-01-26.mdx +++ b/surfsense_web/changelog/content/2026-01-26.mdx @@ -1,6 +1,6 @@ --- title: "SurfSense v0.0.12 - New Main UI, Real-time Collaboration and Comments" -description: "SurfSense v0.0.12 transforms the platform with a redesigned chat-first interface, real-time collaboration features, comment threads with @mentions, and instant notifications powered by ElectricSQL + PGlite." +description: "SurfSense v0.0.12 transforms the platform with a redesigned chat-first interface, real-time collaboration features, comment threads with @mentions, and instant notifications powered by real-time sync." date: "2026-01-26" tags: ["UX", "UI", "Real-time chat", "Collaboration", "Comments"] version: "0.0.12" @@ -40,7 +40,7 @@ This release brings major improvements to **collaboration and user experience**. #### Real-Time Notifications -- **Instant Updates**: Replaced slow polling with instant notifications using ElectricSQL + PGlite +- **Instant Updates**: Replaced slow polling with instant notifications using real-time sync - **New Inbox**: See connector indexing, document processing, and system events immediately - **Cross-Tab Sync**: Syncs across all your browser tabs in real-time From 65539b5381c30bc8e3749628ec5ac27b9c17ba93 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 17:08:21 +0200 Subject: [PATCH 30/31] revert changelog - keep historical ElectricSQL reference --- surfsense_web/changelog/content/2026-01-26.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surfsense_web/changelog/content/2026-01-26.mdx b/surfsense_web/changelog/content/2026-01-26.mdx index d3bbca6c8..e2092bc83 100644 --- a/surfsense_web/changelog/content/2026-01-26.mdx +++ b/surfsense_web/changelog/content/2026-01-26.mdx @@ -1,6 +1,6 @@ --- title: "SurfSense v0.0.12 - New Main UI, Real-time Collaboration and Comments" -description: "SurfSense v0.0.12 transforms the platform with a redesigned chat-first interface, real-time collaboration features, comment threads with @mentions, and instant notifications powered by real-time sync." +description: "SurfSense v0.0.12 transforms the platform with a redesigned chat-first interface, real-time collaboration features, comment threads with @mentions, and instant notifications powered by ElectricSQL + PGlite." date: "2026-01-26" tags: ["UX", "UI", "Real-time chat", "Collaboration", "Comments"] version: "0.0.12" @@ -40,7 +40,7 @@ This release brings major improvements to **collaboration and user experience**. #### Real-Time Notifications -- **Instant Updates**: Replaced slow polling with instant notifications using real-time sync +- **Instant Updates**: Replaced slow polling with instant notifications using ElectricSQL + PGlite - **New Inbox**: See connector indexing, document processing, and system events immediately - **Cross-Tab Sync**: Syncs across all your browser tabs in real-time From 5bf2734e8cfb8610aa3fa1f78346c9d3f9f0cd8a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 24 Mar 2026 17:19:07 +0200 Subject: [PATCH 31/31] add migration to clean up Electric SQL artifacts --- .../108_cleanup_electric_sql_artifacts.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 surfsense_backend/alembic/versions/108_cleanup_electric_sql_artifacts.py 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