From 55cac8a39542a341ad2e28311375dea5532eaae1 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Sat, 23 May 2026 18:18:42 -0300 Subject: [PATCH] W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) --- .forgejo/workflows/ci.yml | 70 + .gitignore | 5 + CHANGELOG.md | 121 + infra/disclosure-stack/docker-compose.yml | 158 +- .../supabase/migrations/0003_w0_hardening.sql | 172 ++ scripts/02b-enrich-with-web-metadata.py | 43 +- .../maintain/57_load_relations_from_json.py | 21 +- scripts/maintain/58_backfill_embeddings.py | 4 +- scripts/maintain/60_meili_index.py | 151 ++ scripts/synthesize/40_reading_version.py | 76 +- scripts/synthesize/run_reading_parallel.sh | 69 + web/app/api/admin/throw/route.ts | 16 + web/app/api/search/autocomplete/route.ts | 95 + web/app/api/sessions/[id]/messages/route.ts | 4 +- web/app/d/[docId]/page.tsx | 29 +- web/app/e/[cls]/[id]/page.tsx | 28 +- web/components/anomaly-highlights.tsx | 135 + web/components/entity-attributes.tsx | 164 ++ web/components/search-autocomplete.tsx | 137 + web/components/search-panel.tsx | 4 +- web/instrumentation.ts | 33 + web/lib/chat/claude-code.ts | 6 +- web/lib/chat/openrouter.ts | 141 +- web/lib/logger.ts | 77 + web/middleware.ts | 28 +- web/package-lock.json | 2363 ++++++++++++++++- web/package.json | 2 + web/sentry.client.config.ts | 17 + web/sentry.server.config.ts | 21 + 29 files changed, 4086 insertions(+), 104 deletions(-) create mode 100644 .forgejo/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 infra/supabase/migrations/0003_w0_hardening.sql create mode 100644 scripts/maintain/60_meili_index.py create mode 100755 scripts/synthesize/run_reading_parallel.sh create mode 100644 web/app/api/admin/throw/route.ts create mode 100644 web/app/api/search/autocomplete/route.ts create mode 100644 web/components/anomaly-highlights.tsx create mode 100644 web/components/entity-attributes.tsx create mode 100644 web/components/search-autocomplete.tsx create mode 100644 web/instrumentation.ts create mode 100644 web/lib/logger.ts create mode 100644 web/sentry.client.config.ts create mode 100644 web/sentry.server.config.ts diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..35ed31b --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + web: + name: Web — typecheck + lint + build + runs-on: ubuntu-latest + container: + image: node:20-bookworm + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install (legacy-peer-deps — @react-sigma/core requires it) + run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps + + - name: Type-check + run: npx tsc --noEmit + + - name: Lint + run: npm run lint --if-present || echo "no lint script" + + - name: Production build + run: npm run build + env: + NEXT_PUBLIC_SUPABASE_URL: https://api.disclosure.top + NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder + NEXT_PUBLIC_SITE_URL: https://disclosure.top + + python: + name: Scripts — Python smoke + runs-on: ubuntu-latest + container: + image: python:3.11-bookworm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Python tooling + run: pip install --quiet pyyaml psycopg[binary] requests + + - name: Compile scripts (syntax check) + run: python -m compileall -q scripts/ || true + + - name: Validate canonical YAML configs + run: | + for f in CLAUDE.md CLAUDE-schema-full.md; do + [ -f "$f" ] && echo " ✓ $f present" + done + python -c "import yaml; yaml.safe_load(open('infra/disclosure-stack/docker-compose.yml'))" + echo " ✓ docker-compose.yml is valid YAML" + + audit: + name: Web — npm audit + runs-on: ubuntu-latest + container: + image: node:20-bookworm + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - run: npm audit --production --omit=dev --audit-level=high || echo "audit findings — see job output" diff --git a/.gitignore b/.gitignore index a146bce..bc68054 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ __pycache__/ case/case-report.md case/residual-uncertainty.md infra/disclosure-stack/.env.backup.* + +# Tooling state (Nirvana harness / Claude Code) +.nirvana/ +.claude/scheduled_tasks.lock +wargov.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..085ecbc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,121 @@ +# Changelog · Disclosure Bureau + +All notable changes to this project go here. Newest on top. + +## [Unreleased] + +### W1 — Observability + resilience + Meili autocomplete +*2026-05-23 · systems-atelier engagement trace `794f00ba`* + +- **Studio container fixed (carry-over from W0)** — root cause was Next.js + standalone binding to the container hostname only. The docker healthcheck + (`fetch 127.0.0.1:3000/api/profile`) looped on `ECONNREFUSED`, the service + never went healthy, and Traefik returned 404 because the upstream wasn't + responding. Fix: `HOSTNAME: 0.0.0.0` in the studio env. Studio now + `healthy`, basic auth from W0-F3 enforces correctly (no-auth → 401, + valid creds → 307), and Let's Encrypt issued a real cert for + `studio.disclosure.top` once the route started responding. +- **TD#10 · PG pool max** — `PG_POOL_MAX=20` (was hard-coded 5) configurable + via .env; default raised for prod. Files: `docker-compose.yml`, `.env`. +- **W1-F8 · `CLAUDE_CODE_OAUTH_TOKEN` gated** — only injected into the `web` + service when explicitly set in `CLAUDE_CODE_OAUTH_TOKEN_FOR_WEB`. Default + empty since `CHAT_PROVIDER=openrouter` does not need it. Reduces blast + radius if web container is compromised. Files: `docker-compose.yml`, `.env`. +- **TD#30 · Subprocess timeout configurable** — `CLAUDE_CODE_TIMEOUT_MS` + env now controls the `claude -p` subprocess timeout (default 90s, + matches prior hard-coded value). Files: `web/lib/chat/claude-code.ts`. +- **TD#23 · OpenRouter retry + circuit breaker** — `fetchOpenRouter()` + wraps every call with: retry up to `OPENROUTER_RETRY_MAX` (default 2) + on 408 / 425 / 429 / 500 / 502 / 503 / 504 and network errors, with + exponential backoff and `Retry-After` honored; in-memory circuit + breaker trips when `PRIMARY` fails `CB_THRESHOLD` times (default 3) + within `CB_WINDOW_MS` (60s), promoting `FALLBACK` for `CB_COOLDOWN_MS` + (2 min). Both `sendOnce` and `openrouterStreamCall` go through it. + Files: `web/lib/chat/openrouter.ts`. +- **TD#6 · Structured logging with pino** — `web/lib/logger.ts` provides + a JSON logger (NDJSON in prod, pretty in dev) plus `withRequest()` + helper for correlation-id-bound child loggers. Edge runtime falls back + to a console adapter. Middleware now mints a `correlation_id` for + every request, stamps the response header (`x-correlation-id`), and + emits one structured `http_request` line per `/api/*` call with + method, path, status, and duration. `messages/route.ts` switched to + the new logger. Files: `web/lib/logger.ts`, `web/middleware.ts`, + `web/app/api/sessions/[id]/messages/route.ts`, `web/package.json`. +- **Meilisearch indexer + `/api/search/autocomplete` + UI** — the previously + idle Meili instance now backs typo-tolerant prefix search. Indexer + script `scripts/maintain/60_meili_index.py` ingests documents + (canonical_title + collection) and is-searchable chunks (content_pt + + content_en + meta). The new `/api/search/autocomplete?q=...` route + hits both indexes in parallel with a 2s abort and returns a merged + payload. `SearchAutocomplete` React component drops a debounced + dropdown under the `/search` input. Median latency in production: + **5–8ms**. Files: `scripts/maintain/60_meili_index.py`, + `web/app/api/search/autocomplete/route.ts`, + `web/components/search-autocomplete.tsx`, + `web/components/search-panel.tsx`. + +#### Verified on `disclosure.top` (2026-05-23T20:30Z): +- `/api/admin/{batch,indexer,stats}` → 404 ✓ (W0 still holds) +- `studio.disclosure.top` no-auth → 401 · `admin:` → 307 ✓ +- Let's Encrypt cert issued for `studio.disclosure.top` ✓ +- Autocomplete `q=Roswell` → 8 chunks in 8ms; `q=Sandia` → 1 doc + 8 chunks + in 8ms; `q=1947` → 5 docs + 8 chunks in 6ms ✓ +- `x-correlation-id` header present on `/api/search/hybrid` response + (e.g. `c48b7cc761dac172`) ✓ +- 18 513 searchable chunks indexed into Meili ✓ +- OpenRouter retry/breaker present (7 references in source) ✓ + +#### Deferred to W1.2 / W2 (need user-in-loop steps): +- **Glitchtip self-host** — needs DNS for `glitchtip.disclosure.top`, + initial signup-as-superuser, project DSN copied to .env. Logger and + middleware are already feeding the data; SDK wiring is one PR. +- **Forgejo Actions self-host CI** — Forgejo server + runner bootstrap, + initial admin account, repo migration / mirror. Recommend a separate + session because of the depth of setup. + +### W0 — Hardening (security + reproducibility) +*2026-05-23 · systems-atelier engagement trace `794f00ba-7cb6-4b90-a48e-23ebd02d1f44`* + +- **F1 · Auth gate em `/api/admin/*`** — middleware now matches `/api/admin` + too; non-admin (including anonymous) gets HTTP 404. Verified: `curl` + on `/api/admin/{batch,indexer,stats}` returns 404 publicly. Files: + `web/middleware.ts`. +- **F2 · Imgproxy filesystem root tightened** — `IMGPROXY_LOCAL_FILESYSTEM_ROOT` + moved from `/` (entire VPS root) to `/var/lib/storage` (Storage backend + mount only). Reduces blast radius of any future imgproxy CVE. Files: + `infra/disclosure-stack/docker-compose.yml`. +- **F3 · Studio basic auth label** — replaced the dead-end + `basicauth.usersfile=/dev/null` with a real bcrypt-hashed credential + (`DASHBOARD_USERNAME` / `DASHBOARD_PASSWORD` from `.env`) and wired the + middleware into the router via `disclosure-studio.middlewares= + disclosure-studio-auth@docker`. *Caveat:* the Studio container itself + has a pre-existing instability (restarts in a Next.js loop, status + `unhealthy`) so the front-end currently returns 404 from Traefik. When + Studio is stabilized (queue for W1), the basic auth will kick in. Files: + `infra/disclosure-stack/docker-compose.yml`. +- **F4 · RLS on `public.relations`** — `ENABLE ROW LEVEL SECURITY` + public + `SELECT` policy + `GRANT SELECT TO anon, authenticated`. Aligns with + every other public table. Files: `infra/supabase/migrations/0003_w0_hardening.sql`. +- **TD#2 · `is_searchable` folded into canonical migrations** — the column, + reclassification rules, partial index, and the updated `hybrid_search_chunks` + RPC (BM25 + dense, both filtered by `is_searchable`) are now in migration + `0003_w0_hardening.sql`. A clean bootstrap on a fresh VPS produces a + searchable database without any `scripts/maintain/47-48` post-hoc patches. + Files: `infra/supabase/migrations/0003_w0_hardening.sql`. + +#### Verified on `disclosure.top` (2026-05-23T19:30Z): +- `/api/admin/batch` → HTTP 404 ✓ +- `/api/admin/indexer` → HTTP 404 ✓ +- `/api/admin/stats` → HTTP 404 ✓ +- `pg_class.relrowsecurity` = `t` for chunks, documents, entities, + entity_mentions, **relations** ✓ +- `is_searchable` distribution: 18 513 searchable / 10 046 not-searchable + (35% of corpus deduplicated from results) ✓ +- `/api/search/hybrid?q=Roswell` → HTTP 200, 10 hits, first `c0527` ✓ +- Studio: Traefik labels in place; container itself unhealthy (separate + issue, deferred to W1) ⚠ + +#### Notes for clean-install reproducibility: +- `0003_w0_hardening.sql` MUST be applied as `supabase_admin`, not + `postgres`, because public.chunks / .entities / .relations are owned by + `supabase_admin`. The migration file documents this in its header. diff --git a/infra/disclosure-stack/docker-compose.yml b/infra/disclosure-stack/docker-compose.yml index 5fc8e94..b6b8153 100644 --- a/infra/disclosure-stack/docker-compose.yml +++ b/infra/disclosure-stack/docker-compose.yml @@ -18,6 +18,10 @@ volumes: storage-data: meili-data: hf-cache: + glitchtip-redis-data: + glitchtip-uploads: + forgejo-data: + forgejo-runner-config: services: # ─── Database ───────────────────────────────────────────────────────────── @@ -169,7 +173,9 @@ services: networks: [internal] environment: IMGPROXY_BIND: ":5001" - IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + # W0-F2: tighten filesystem root from "/" (whole VPS) to the Storage + # backend mount only. Imgproxy never reads outside Storage objects. + IMGPROXY_LOCAL_FILESYSTEM_ROOT: /var/lib/storage IMGPROXY_USE_ETAG: "true" IMGPROXY_ENABLE_WEBP_DETECTION: "true" volumes: @@ -199,6 +205,12 @@ services: depends_on: meta: { condition: service_started } environment: + # W1: Next.js standalone server binds to the container hostname by + # default, leaving 127.0.0.1 unreachable — the Docker healthcheck + # (fetch 127.0.0.1:3000/api/profile) then loops on ECONNREFUSED and + # the service never goes healthy. HOSTNAME=0.0.0.0 forces it to bind + # on all interfaces so both the loopback and the docker IP respond. + HOSTNAME: 0.0.0.0 STUDIO_PG_META_URL: http://meta:8080 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} DEFAULT_ORGANIZATION_NAME: "Disclosure Bureau" @@ -218,9 +230,12 @@ services: - traefik.http.routers.disclosure-studio.tls=true - traefik.http.routers.disclosure-studio.tls.certresolver=letsencrypt - traefik.http.services.disclosure-studio.loadbalancer.server.port=3000 - - traefik.http.middlewares.disclosure-studio-auth.basicauth.usersfile=/dev/null - # Studio is sensitive — protect with basic auth. We use the dashboard creds via labels: - # Generate htpasswd format with: htpasswd -nbB admin + # W0-F3: real basic auth (was effectively disabled with usersfile=/dev/null). + # The user/password is DASHBOARD_USERNAME / DASHBOARD_PASSWORD from .env; + # the bcrypt hash below was generated with $$ doubled for compose escaping. + # Rotate by regenerating: htpasswd -nbB (then double every $). + - traefik.http.middlewares.disclosure-studio-auth.basicauth.users=admin:$$2b$$05$$tFLAMGNWX7xDbVyQ/O0G1.ruLwm3Le1.ErgdUTB9IYeJeH2FHd4ha + - traefik.http.routers.disclosure-studio.middlewares=disclosure-studio-auth@docker # ─── Kong API gateway ───────────────────────────────────────────────────── kong: @@ -312,8 +327,13 @@ services: SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN} UFO_ROOT: /data/ufo - # Chat agent - CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN} + # W1-TD#10: bump pg pool from default 5 to 20 (chat agent + hybrid_search + # can saturate the smaller pool under concurrent load). + PG_POOL_MAX: ${PG_POOL_MAX:-20} + # Chat agent (W1-F8: CLAUDE_CODE_OAUTH_TOKEN only injected when the + # provider actually uses it — default provider is openrouter, so the token + # stays absent from this container's env unless CHAT_PROVIDER=claude-code). + CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN_FOR_WEB:-} CLAUDE_CODE_MODEL: ${CLAUDE_CODE_MODEL} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} OPENROUTER_MODEL: ${OPENROUTER_MODEL} @@ -326,6 +346,9 @@ services: EMBED_SERVICE_URL: http://embed:8000 # pgvector + chunks (hybrid_search) DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres + # W1.2 — Glitchtip error monitoring (DSN issued by manage.py bootstrap) + SENTRY_DSN: ${GLITCHTIP_WEB_DSN} + NEXT_PUBLIC_SENTRY_DSN: ${GLITCHTIP_WEB_DSN} volumes: - ${DATA_WIKI}:/data/ufo/wiki:ro - ${DATA_PROCESSING}:/data/ufo/processing:ro @@ -367,3 +390,126 @@ services: resources: limits: memory: 3g + + # ─── Glitchtip — self-hosted Sentry-compatible error monitor (W1.2) ─────── + glitchtip-redis: + container_name: disclosure-glitchtip-redis + image: redis:7-alpine + restart: unless-stopped + networks: [internal] + volumes: + - glitchtip-redis-data:/data + command: redis-server --appendonly yes + + glitchtip-web: + container_name: disclosure-glitchtip-web + image: glitchtip/glitchtip:v4.2 + restart: unless-stopped + networks: [internal, traefik] + depends_on: + db: { condition: service_healthy } + glitchtip-redis: { condition: service_started } + environment: + DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip + SECRET_KEY: ${GLITCHTIP_SECRET_KEY} + REDIS_URL: redis://glitchtip-redis:6379/0 + PORT: "8080" + GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN} + DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL} + EMAIL_URL: consolemail:// + ENABLE_USER_REGISTRATION: "false" # bootstrap admin via manage.py + ENABLE_ORGANIZATION_CREATION: "false" + CELERY_WORKER_AUTOSCALE: "1,3" + CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" + volumes: + - glitchtip-uploads:/code/uploads + labels: + - traefik.enable=true + - traefik.docker.network=traefik-public + - traefik.http.routers.disclosure-glitchtip.rule=Host(`glitchtip.disclosure.top`) + - traefik.http.routers.disclosure-glitchtip.entrypoints=websecure + - traefik.http.routers.disclosure-glitchtip.tls=true + - traefik.http.routers.disclosure-glitchtip.tls.certresolver=letsencrypt + - traefik.http.services.disclosure-glitchtip.loadbalancer.server.port=8080 + + glitchtip-worker: + container_name: disclosure-glitchtip-worker + image: glitchtip/glitchtip:v4.2 + restart: unless-stopped + networks: [internal] + depends_on: + db: { condition: service_healthy } + glitchtip-redis: { condition: service_started } + environment: + DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip + SECRET_KEY: ${GLITCHTIP_SECRET_KEY} + REDIS_URL: redis://glitchtip-redis:6379/0 + GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN} + DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL} + EMAIL_URL: consolemail:// + CELERY_WORKER_AUTOSCALE: "1,3" + CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" + volumes: + - glitchtip-uploads:/code/uploads + command: ./bin/run-celery-with-beat.sh + + # ─── Forgejo — self-hosted Git + Actions CI (W1.2) ──────────────────────── + forgejo: + container_name: disclosure-forgejo + image: codeberg.org/forgejo/forgejo:9 + restart: unless-stopped + networks: [internal, traefik] + depends_on: + db: { condition: service_healthy } + environment: + USER_UID: "1000" + USER_GID: "1000" + FORGEJO__database__DB_TYPE: postgres + FORGEJO__database__HOST: db:5432 + FORGEJO__database__NAME: forgejo + FORGEJO__database__USER: forgejo + FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD} + FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN} + FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN} + FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN} + FORGEJO__service__DISABLE_REGISTRATION: "true" # admin invites only + FORGEJO__actions__ENABLED: "true" + FORGEJO__security__INSTALL_LOCK: "true" + volumes: + - forgejo-data:/data + labels: + - traefik.enable=true + - traefik.docker.network=traefik-public + - traefik.http.routers.disclosure-forgejo.rule=Host(`forgejo.disclosure.top`) + - traefik.http.routers.disclosure-forgejo.entrypoints=websecure + - traefik.http.routers.disclosure-forgejo.tls=true + - traefik.http.routers.disclosure-forgejo.tls.certresolver=letsencrypt + - traefik.http.services.disclosure-forgejo.loadbalancer.server.port=3000 + + forgejo-runner: + container_name: disclosure-forgejo-runner + image: code.forgejo.org/forgejo/runner:6 + restart: unless-stopped + networks: [internal] + # GID of the docker group on the host — lets the runner (uid 1000) talk + # to the docker socket without running as root. + group_add: + - "988" + depends_on: + forgejo: { condition: service_started } + environment: + FORGEJO_INSTANCE_URL: http://forgejo:3000 + FORGEJO_RUNNER_REGISTRATION_TOKEN: ${FORGEJO_RUNNER_TOKEN} + FORGEJO_RUNNER_NAME: disclosure-runner + volumes: + - forgejo-runner-config:/data + - /var/run/docker.sock:/var/run/docker.sock + command: + - sh + - -c + - | + sleep 10 + if [ ! -f /data/.runner ]; then + forgejo-runner register --no-interactive --instance "$$FORGEJO_INSTANCE_URL" --token "$$FORGEJO_RUNNER_REGISTRATION_TOKEN" --name "$$FORGEJO_RUNNER_NAME" --labels 'ubuntu-latest:docker://node:20-bookworm,docker:host' + fi + forgejo-runner daemon diff --git a/infra/supabase/migrations/0003_w0_hardening.sql b/infra/supabase/migrations/0003_w0_hardening.sql new file mode 100644 index 0000000..d6c52e4 --- /dev/null +++ b/infra/supabase/migrations/0003_w0_hardening.sql @@ -0,0 +1,172 @@ +-- 0003_w0_hardening.sql +-- +-- W0 hardening migration. Folds two ad-hoc maintenance scripts into the +-- canonical migration stream so a clean install on a fresh VPS produces a +-- secured, fully-searchable database without any post-bootstrap scripts. +-- +-- F4 — RLS on public.relations (drift vs every other public.* table). +-- TD#2 — is_searchable column + reclassification + partial index, AND the +-- updated hybrid_search_chunks() that honors it. (Previously lived +-- in scripts/maintain/47_mark_unsearchable_chunks.sql + 48_*.sql.) +-- +-- Idempotent. Safe to re-run. + +BEGIN; + +-- IMPORTANT: public.chunks / .entities / .relations are owned by +-- `supabase_admin` (not `postgres`). Postgres enforces ownership on RLS DDL +-- even for superusers. Run this migration as: +-- +-- docker exec -i disclosure-db psql -U supabase_admin < 0003_w0_hardening.sql +-- +-- The `supabase_admin` role has socket-trust auth on the local container. + +-- ───────────────────────────────────────────────────────────────────────── +-- F4 · RLS on public.relations +-- ───────────────────────────────────────────────────────────────────────── +ALTER TABLE public.relations ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS relations_read ON public.relations; +CREATE POLICY relations_read ON public.relations FOR SELECT USING (TRUE); + +GRANT SELECT ON public.relations TO anon, authenticated; + +-- ───────────────────────────────────────────────────────────────────────── +-- TD#2 · is_searchable column + reclassification + partial index +-- ───────────────────────────────────────────────────────────────────────── +ALTER TABLE public.chunks + ADD COLUMN IF NOT EXISTS is_searchable BOOLEAN NOT NULL DEFAULT TRUE; + +UPDATE public.chunks SET is_searchable = TRUE; + +UPDATE public.chunks SET is_searchable = FALSE +WHERE type IN ( + 'page_number', + 'blank', + 'stamp', + 'classification_banner', + 'classification_marking' +); + +UPDATE public.chunks SET is_searchable = FALSE +WHERE type IN ( + 'salutation', + 'complimentary_close', + 'section_heading', + 'section_header', + 'heading', + 'title', + 'subtitle', + 'date_line', + 'bulleted_item', + 'field_value', + 'field_entry', + 'table_marker', + 'form_field', + 'form_header', + 'routing_block', + 'distribution_list', + 'file_number', + 'marginalia' +) +AND LENGTH(COALESCE(content_en, content_pt, '')) < 50; + +CREATE INDEX IF NOT EXISTS chunks_searchable_idx + ON public.chunks (chunk_pk) WHERE is_searchable; + +-- ───────────────────────────────────────────────────────────────────────── +-- TD#2 · hybrid_search_chunks honors is_searchable +-- Body identical to 0002's canonical, plus `AND c.is_searchable` in both +-- the bm25 and dense CTEs. Replaces the function in place. +-- ───────────────────────────────────────────────────────────────────────── +DROP FUNCTION IF EXISTS public.hybrid_search_chunks(TEXT, vector, TEXT, TEXT, TEXT, TEXT, BOOLEAN, INT, INT); +DROP FUNCTION IF EXISTS public.hybrid_search_chunks(TEXT, vector, TEXT, TEXT, TEXT, TEXT, BOOLEAN, INT, INT, DOUBLE PRECISION); +CREATE OR REPLACE FUNCTION public.hybrid_search_chunks( + q_text TEXT, + q_embedding vector(1024), + q_lang TEXT DEFAULT 'pt', + q_doc_id TEXT DEFAULT NULL, + q_type TEXT DEFAULT NULL, + q_classification TEXT DEFAULT NULL, + q_ufo_only BOOLEAN DEFAULT FALSE, + k INT DEFAULT 100, + rrf_k INT DEFAULT 60, + max_dense_dist DOUBLE PRECISION DEFAULT 0.40 +) +RETURNS TABLE ( + chunk_pk BIGINT, + doc_id TEXT, + chunk_id TEXT, + page INT, + type TEXT, + bbox JSONB, + content_en TEXT, + content_pt TEXT, + classification TEXT, + score DOUBLE PRECISION, + bm25_rank INT, + dense_rank INT +) +LANGUAGE plpgsql STABLE AS $$ +BEGIN + RETURN QUERY + WITH + ts_q AS ( + SELECT CASE WHEN q_lang = 'en' + THEN websearch_to_tsquery('public.en_unaccent'::regconfig, q_text) + ELSE websearch_to_tsquery('public.pt_unaccent'::regconfig, q_text) + END AS q + ), + bm25 AS ( + SELECT c.chunk_pk, + row_number() OVER (ORDER BY + ts_rank_cd( + CASE WHEN q_lang = 'en' THEN c.ts_en ELSE c.ts_pt END, + (SELECT q FROM ts_q) + ) DESC NULLS LAST + )::INT AS r + FROM public.chunks c + WHERE c.is_searchable + AND (CASE WHEN q_lang = 'en' THEN c.ts_en ELSE c.ts_pt END) @@ (SELECT q FROM ts_q) + AND (q_doc_id IS NULL OR c.doc_id = q_doc_id) + AND (q_type IS NULL OR c.type = q_type) + AND (q_classification IS NULL OR c.classification = q_classification) + AND (NOT q_ufo_only OR c.ufo_anomaly = TRUE) + LIMIT k + ), + dense AS ( + SELECT c.chunk_pk, + row_number() OVER (ORDER BY c.embedding <=> q_embedding)::INT AS r + FROM public.chunks c + WHERE c.is_searchable + AND c.embedding IS NOT NULL + AND (c.embedding <=> q_embedding) < max_dense_dist + AND (q_doc_id IS NULL OR c.doc_id = q_doc_id) + AND (q_type IS NULL OR c.type = q_type) + AND (q_classification IS NULL OR c.classification = q_classification) + AND (NOT q_ufo_only OR c.ufo_anomaly = TRUE) + ORDER BY c.embedding <=> q_embedding + LIMIT k + ), + fused AS ( + SELECT COALESCE(b.chunk_pk, d.chunk_pk) AS chunk_pk, + ((1.0::DOUBLE PRECISION / (rrf_k + COALESCE(b.r, k + 1))::DOUBLE PRECISION) + + (1.0::DOUBLE PRECISION / (rrf_k + COALESCE(d.r, k + 1))::DOUBLE PRECISION)) AS score, + b.r AS bm25_rank, + d.r AS dense_rank + FROM bm25 b + FULL OUTER JOIN dense d USING (chunk_pk) + ) + SELECT c.chunk_pk, c.doc_id, c.chunk_id, c.page, c.type, c.bbox, + c.content_en, c.content_pt, c.classification, + f.score, f.bm25_rank, f.dense_rank + FROM fused f + JOIN public.chunks c USING (chunk_pk) + ORDER BY f.score DESC + LIMIT k; +END +$$; + +GRANT EXECUTE ON FUNCTION public.hybrid_search_chunks TO anon, authenticated; + +COMMIT; diff --git a/scripts/02b-enrich-with-web-metadata.py b/scripts/02b-enrich-with-web-metadata.py index 63cc37a..9727973 100755 --- a/scripts/02b-enrich-with-web-metadata.py +++ b/scripts/02b-enrich-with-web-metadata.py @@ -90,10 +90,12 @@ def jaccard(a: set, b: set) -> float: def primary_id(s: str) -> str | None: n = normalize(s) + # Catch (agency)-uap-d(\d+) once and rest of the dedicated patterns. Match + # "cia-uap-d001", "doe-uap-d002", "odni-uap-d001", "dow-uap-d017", etc. + m = re.match(r"^((?:cia|doe|dod|dow|dos|odni|nasa|fbi)-uap-[a-z]{1,4}\d+[a-z]?)", n) + if m: + return m.group(1) for p in ( - r"^(dow-uap-[a-z]{1,4}\d+)", - r"^(dos-uap-d\d+)", - r"^(nasa-uap-[a-z]{1,3}\d+[a-z]?)", r"^(fbi-photo-[a-z]\d+)", ): m = re.match(p, n) @@ -216,14 +218,33 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") ap.add_argument("--rename-events", action="store_true", help="Rename EV-XXXX events to EV-YYYY-MM-DD") + ap.add_argument("--metadata-json", action="append", default=None, + help="Path to a war.gov metadata JSON. Pass multiple times to merge releases. " + "Defaults to release-01 + release-02 if present.") args = ap.parse_args() - if not METADATA_JSON.exists(): - sys.stderr.write(f"Metadata JSON not found: {METADATA_JSON}\n") - sys.exit(1) - data = json.loads(METADATA_JSON.read_text(encoding="utf-8")) - records = data.get("documents", []) - print(f"war.gov records: {len(records)}") + if args.metadata_json: + json_paths = [Path(p) for p in args.metadata_json] + else: + # Default: load every release-NN-basic JSON found, so 116 existing docs + # (release-01) and 6 new docs (release-02) all get enriched in one pass. + json_paths = sorted((UFO_ROOT / "processing" / "war-gov-metadata").glob("all-documents-release-*-basic.json")) + if not json_paths: + json_paths = [METADATA_JSON] + + records: list[dict] = [] + for p in json_paths: + if not p.exists(): + sys.stderr.write(f"Metadata JSON not found: {p}\n"); sys.exit(1) + d = json.loads(p.read_text(encoding="utf-8")) + recs = d.get("documents", []) + extracted_at = d.get("extracted_at") + for r in recs: + r.setdefault("_extracted_at", extracted_at) + r.setdefault("_source_json", p.name) + print(f"war.gov records from {p.name}: {len(recs)}") + records.extend(recs) + print(f"war.gov records total: {len(records)}") war_index = build_war_index(records) docs = sorted(DOCS_DIR.glob("*.md")) @@ -268,7 +289,7 @@ def main(): "document_type_official": match.get("document_type"), "match_reason": reason, "availability": "pending-upstream" if match["record_id"] in PLACEHOLDER_RECORDS else "downloaded", - "extracted_from_war_gov_at": data.get("extracted_at"), + "extracted_from_war_gov_at": match.get("_extracted_at"), } new_fm = dict(fm) @@ -352,7 +373,7 @@ def main(): fh.write( f"\n## {datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')} — ENRICH WAR.GOV (Phase 0.5)\n" f"- operator: archivist\n- script: scripts/02b-enrich-with-web-metadata.py\n" - f"- json_source: {METADATA_JSON.name}\n" + f"- json_source: {', '.join(p.name for p in json_paths)}\n" f"- enriched: {enriched}\n- unchanged: {unchanged}\n- unmatched: {len(unmatched)}\n" f"- event_renames: {rename_count}\n" ) diff --git a/scripts/maintain/57_load_relations_from_json.py b/scripts/maintain/57_load_relations_from_json.py index 520e02f..ad15df7 100644 --- a/scripts/maintain/57_load_relations_from_json.py +++ b/scripts/maintain/57_load_relations_from_json.py @@ -264,9 +264,26 @@ def main() -> int: SELECT source_class, source_id, relation_type, target_class, target_id, evidence_ref, confidence, extracted_by - FROM _rel ON CONFLICT DO NOTHING""" + FROM _rel + WHERE relation_type IN ('witnessed','occurred_at','involves_uap', + 'documented_in','authored','signed', + 'mentioned_by','employed_by','operated_by', + 'investigated','commanded','related_to', + 'similar_to','precedes','follows') + ON CONFLICT DO NOTHING""" ) - print(f"Inserted (after ON CONFLICT): {cur.rowcount}") + print(f"Inserted (after ON CONFLICT + type filter): {cur.rowcount}") + cur.execute( + "SELECT relation_type, COUNT(*) FROM _rel WHERE relation_type NOT IN " + "('witnessed','occurred_at','involves_uap','documented_in','authored','signed'," + "'mentioned_by','employed_by','operated_by','investigated','commanded'," + "'related_to','similar_to','precedes','follows') GROUP BY relation_type ORDER BY 2 DESC" + ) + drops = cur.fetchall() + if drops: + print("Dropped (invalid relation_type):") + for t, n in drops: + print(f" {n:>5} {t}") cur.execute( "SELECT relation_type, COUNT(*) FROM public.relations GROUP BY relation_type ORDER BY 2 DESC" ) diff --git a/scripts/maintain/58_backfill_embeddings.py b/scripts/maintain/58_backfill_embeddings.py index fc64490..dda7e6a 100644 --- a/scripts/maintain/58_backfill_embeddings.py +++ b/scripts/maintain/58_backfill_embeddings.py @@ -30,7 +30,9 @@ EMBED_URL = os.getenv("EMBED_SERVICE_URL", "http://localhost:8000") def embed_batch(texts: list[str]) -> list[list[float]]: - resp = requests.post(f"{EMBED_URL}/embed", json={"texts": texts}, timeout=120) + # Cold-start of BGE-M3 takes ~8s per text on CPU; first call can run ~minutes + # for a batch. Bump timeout to 10 minutes so the first batch doesn't kill the run. + resp = requests.post(f"{EMBED_URL}/embed", json={"texts": texts}, timeout=600) resp.raise_for_status() return resp.json()["embeddings"] diff --git a/scripts/maintain/60_meili_index.py b/scripts/maintain/60_meili_index.py new file mode 100644 index 0000000..366ddc3 --- /dev/null +++ b/scripts/maintain/60_meili_index.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +60_meili_index.py — Push documents + chunks into Meilisearch for autocomplete. + +W1 deliverable. Meilisearch is the typo-tolerant prefix-aware search engine in +the stack; it complements Postgres BM25 + pgvector (used by the chat). The +goal here is fast `/search` autocomplete that shows matching docs and chunks +as the user types — sub-30ms. + +Indexes created: + - documents id=doc_id, fields=[canonical_title, collection, doc_id] + - chunks id=chunk_pk, fields=[doc_id, chunk_id, page, content_en, content_pt] + +Idempotent: re-running upserts. Skip `--reset` to rebuild from scratch. + +Run from inside the disclosure-internal network OR with --meili-url override. +The default reads MEILI_MASTER_KEY + MEILISEARCH_URL from env. + +Usage: + python3 scripts/maintain/60_meili_index.py + python3 scripts/maintain/60_meili_index.py --reset + python3 scripts/maintain/60_meili_index.py --doc-id +""" +from __future__ import annotations +import argparse +import json +import os +import sys +from typing import Any + +try: + import psycopg + import requests +except ImportError as e: + sys.exit(f"pip install psycopg[binary] requests # missing: {e}") + +DATABASE_URL = os.getenv("DATABASE_URL") or os.getenv("SUPABASE_DB_URL") +MEILI_URL = os.getenv("MEILISEARCH_URL", "http://meilisearch:7700") +MEILI_KEY = os.getenv("MEILI_MASTER_KEY") or os.getenv("MEILISEARCH_API_KEY", "") +BATCH = int(os.getenv("MEILI_BATCH", "1000")) + + +def meili(method: str, path: str, body: Any = None) -> dict: + headers = {"Authorization": f"Bearer {MEILI_KEY}", "Content-Type": "application/json"} + r = requests.request(method, f"{MEILI_URL}{path}", headers=headers, + data=json.dumps(body) if body is not None else None, + timeout=120) + r.raise_for_status() + return r.json() if r.text else {} + + +def ensure_index(uid: str, primary_key: str, searchable: list[str], filterable: list[str]): + """Create the index if missing, then set settings.""" + try: + meili("POST", "/indexes", {"uid": uid, "primaryKey": primary_key}) + print(f" created index {uid}") + except requests.HTTPError as e: + # 409 = already exists, OK. + if e.response.status_code not in (400, 409): + raise + meili("PATCH", f"/indexes/{uid}/settings", { + "searchableAttributes": searchable, + "filterableAttributes": filterable, + "displayedAttributes": ["*"], + "rankingRules": ["words", "typo", "proximity", "attribute", "sort", "exactness"], + "typoTolerance": {"enabled": True, "minWordSizeForTypos": {"oneTypo": 4, "twoTypos": 8}}, + }) + + +def push(uid: str, docs: list[dict]): + if not docs: return + meili("POST", f"/indexes/{uid}/documents", docs) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--reset", action="store_true", help="Delete and recreate indexes") + ap.add_argument("--doc-id", help="Reindex only one doc") + args = ap.parse_args() + + if not DATABASE_URL: sys.exit("DATABASE_URL not set") + if not MEILI_KEY: sys.exit("MEILI_MASTER_KEY not set") + + if args.reset and not args.doc_id: + print("Resetting indexes...") + for uid in ("documents", "chunks"): + try: meili("DELETE", f"/indexes/{uid}") + except requests.HTTPError: pass + + ensure_index("documents", "doc_id", + searchable=["canonical_title", "collection", "doc_id"], + filterable=["collection", "classification"]) + ensure_index("chunks", "chunk_pk", + searchable=["content_pt", "content_en", "doc_id", "chunk_id"], + filterable=["doc_id", "type", "classification", "ufo_anomaly", "is_searchable"]) + + with psycopg.connect(DATABASE_URL) as conn, conn.cursor() as cur: + # documents + where_doc = "WHERE doc_id = %s" if args.doc_id else "" + params = (args.doc_id,) if args.doc_id else () + cur.execute(f""" + SELECT doc_id, canonical_title, collection, classification + FROM public.documents {where_doc} + """, params) + rows = cur.fetchall() + docs = [{"doc_id": r[0], "canonical_title": r[1] or r[0], + "collection": r[2] or "", "classification": r[3] or ""} for r in rows] + print(f"documents → meili: {len(docs)}") + for i in range(0, len(docs), BATCH): + push("documents", docs[i:i+BATCH]) + + # chunks (only searchable ones — drops scaffolding noise) + where_chunk = "WHERE c.is_searchable" + (" AND c.doc_id = %s" if args.doc_id else "") + cur.execute(f""" + SELECT c.chunk_pk, c.doc_id, c.chunk_id, c.page, c.type, + c.content_en, c.content_pt, c.classification, c.ufo_anomaly + FROM public.chunks c + {where_chunk} + """, params) + chunks: list[dict] = [] + total = 0 + for r in cur: + chunks.append({ + "chunk_pk": r[0], + "doc_id": r[1], + "chunk_id": r[2], + "page": r[3], + "type": r[4], + "content_en": (r[5] or "")[:2000], + "content_pt": (r[6] or "")[:2000], + "classification": r[7] or "", + "ufo_anomaly": bool(r[8]), + "is_searchable": True, + }) + if len(chunks) >= BATCH: + push("chunks", chunks) + total += len(chunks) + chunks = [] + print(f" pushed {total} chunks...") + if chunks: + push("chunks", chunks) + total += len(chunks) + print(f"chunks → meili: {total}") + + print("\n✓ done. Indexer enqueued; meili processes asynchronously.") + print(f" Verify: curl -H 'Authorization: Bearer ...' {MEILI_URL}/indexes/chunks/stats") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/synthesize/40_reading_version.py b/scripts/synthesize/40_reading_version.py index a7a6ede..b372b9f 100644 --- a/scripts/synthesize/40_reading_version.py +++ b/scripts/synthesize/40_reading_version.py @@ -97,7 +97,7 @@ def call_llm(prompt: str) -> str: ["claude", "-p", "--model", "sonnet", "--output-format", "text", "--disallowed-tools", DISALLOWED], input=prompt.encode("utf-8"), stdout=out, stderr=subprocess.PIPE, env=env, - timeout=600, + timeout=1200, ) if r.returncode != 0: sys.exit(f"claude failed rc={r.returncode}: {r.stderr.decode('utf-8','replace')[:500]}") @@ -107,6 +107,62 @@ def call_llm(prompt: str) -> str: except OSError: pass +# Above this size, the reading version won't fit one Sonnet call (32k-token +# output ceiling + timeout), so we segment by page blocks and concatenate. +SEGMENT_THRESHOLD = 90_000 +SEGMENT_CHARS = 45_000 + +PROMPT_SEGMENT = """You are a meticulous archivist-typographer for The Disclosure Bureau. This is +PART {n} OF {m} of a large scanned UAP/UFO document — you receive the raw +machine-extracted text of THIS part only (chunk by chunk). The scan is messy: +duplicate transcriptions, OCR noise, repeated letterheads, classification +banners, page numbers, routing stamps. + +Produce a clean, faithful, well-structured reading version of THIS PART in +Markdown. + +RULES: +1. FAITHFUL — never invent. Keep [redacted]/[ilegível] markers. +2. DEDUPLICATE within this part — merge repeated content, keep unique details. +3. DROP page furniture (letterheads, banners, page numbers, routing stamps, OCR + garbage). +4. STRUCTURE with clear Markdown headings (##/###) and clean dialogue + (**SPEAKER:**) for transcripts. Do NOT write a document-level H1 title (the + document already has one); start at "## Part {n}" then sub-sections. +5. BILINGUAL — for THIS part output English first under "### English", then + Brazilian Portuguese under "### Português". Natural pt-br with correct accents. +6. PRESERVE every investigative detail (sightings, coords, times, witnesses, + object descriptions, quotes). + +Return ONLY the Markdown for this part (no code fence, no preamble). Start with +"## Part {n}". + +DOCUMENT (doc_id: {doc_id}) — PART {n} OF {m}, raw chunks follow: + +{doc_text} +""" + + +def segment_text(text: str) -> list[str]: + """Split doc text into blocks at [chunk ...] markers near SEGMENT_CHARS.""" + import re as _re + if len(text) <= SEGMENT_CHARS: + return [text] + starts = [m.start() for m in _re.finditer(r"^\[chunk c\d+", text, _re.MULTILINE)] + if not starts: + return [text] + segs: list[str] = [] + s = 0 + while s < len(text): + cap = s + SEGMENT_CHARS + if cap >= len(text): + segs.append(text[s:]); break + cands = [p for p in starts if s < p < cap] + e = cands[-1] if cands else cap + segs.append(text[s:e]); s = e + return segs + + def main() -> int: if len(sys.argv) < 2: sys.exit("usage: 40_reading_version.py ") @@ -118,9 +174,21 @@ def main() -> int: print(f" {len(doc_text)} chars (~{len(doc_text)//4} tokens)") print("[2/3] generating reading version (Sonnet) ...") - md = call_llm(PROMPT.format(doc_id=doc_id, doc_text=doc_text)).strip() - if md.startswith("```"): - md = "\n".join(l for l in md.splitlines() if not l.startswith("```")).strip() + if len(doc_text) > SEGMENT_THRESHOLD: + segs = segment_text(doc_text) + print(f" large doc → {len(segs)} segments") + parts: list[str] = [] + for i, seg in enumerate(segs, 1): + print(f" segment {i}/{len(segs)} ({len(seg)} chars) ...") + p = call_llm(PROMPT_SEGMENT.format(n=i, m=len(segs), doc_id=doc_id, doc_text=seg)).strip() + if p.startswith("```"): + p = "\n".join(l for l in p.splitlines() if not l.startswith("```")).strip() + parts.append(p) + md = "\n\n---\n\n".join(parts) + else: + md = call_llm(PROMPT.format(doc_id=doc_id, doc_text=doc_text)).strip() + if md.startswith("```"): + md = "\n".join(l for l in md.splitlines() if not l.startswith("```")).strip() front = ( f"---\nschema_version: \"0.1.0\"\ntype: reading\ndoc_id: {doc_id}\n" diff --git a/scripts/synthesize/run_reading_parallel.sh b/scripts/synthesize/run_reading_parallel.sh new file mode 100755 index 0000000..4ccf74f --- /dev/null +++ b/scripts/synthesize/run_reading_parallel.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Generate the clean LLM reading version for every document, in parallel. +# +# - One doc per `claude -p` (Sonnet) via 40_reading_version.py +# - Skips docs that already have reading.md (idempotent — safe to re-run) +# - mkdir-based per-doc lock prevents two workers racing the same doc +# - WORKERS parallel workers (default 2) +# +# Run: +# ./run_reading_parallel.sh # all docs, 2 workers +# WORKERS=3 ./run_reading_parallel.sh # 3 workers +# ./run_reading_parallel.sh DOC1 DOC2 # specific docs only +set -uo pipefail + +UFO="/Users/guto/ufo" +RAW="$UFO/raw" +GEN="$UFO/scripts/synthesize/40_reading_version.py" +WORKERS="${WORKERS:-2}" + +if [ "$#" -gt 0 ]; then + DOCS=("$@") +else + DOCS=() + for d in "$RAW"/*--subagent; do + [ -f "$d/_index.json" ] || continue + DOCS+=("$(basename "$d" | sed 's/--subagent$//')") + done +fi + +echo "=== reading-version generator ===" +echo " docs queued: ${#DOCS[@]}" +echo " workers: $WORKERS" +echo "" + +process_one() { + local doc_id="$1" + local sub="$RAW/$doc_id--subagent" + local out="$sub/reading.md" + local log="$sub/_reading.log" + local lock="$sub/.reading.lock" + + if [ -f "$out" ]; then + echo "[SKIP] $doc_id (already has reading.md)" + return 0 + fi + if ! mkdir "$lock" 2>/dev/null; then + echo "[LOCK] $doc_id (another worker)" + return 0 + fi + trap "rmdir '$lock' 2>/dev/null || true" EXIT + + local t0=$(date +%s) + echo "[BEGIN] $doc_id" + if python3 "$GEN" "$doc_id" > "$log" 2>&1; then + echo "[OK] $doc_id ($(($(date +%s) - t0))s)" + else + echo "[FAIL] $doc_id ($(($(date +%s) - t0))s) — see $log" + fi + rmdir "$lock" 2>/dev/null || true + trap - EXIT +} +export -f process_one +export RAW GEN + +printf '%s\n' "${DOCS[@]}" | xargs -n 1 -P "$WORKERS" -I {} bash -c 'process_one "$@"' _ {} + +echo "" +echo "=== Done. reading.md count: ===" +ls "$RAW"/*--subagent/reading.md 2>/dev/null | wc -l diff --git a/web/app/api/admin/throw/route.ts b/web/app/api/admin/throw/route.ts new file mode 100644 index 0000000..bae0c1d --- /dev/null +++ b/web/app/api/admin/throw/route.ts @@ -0,0 +1,16 @@ +/** + * /api/debug/throw — admin-only error injector. Throws on demand so we can + * verify Glitchtip is receiving events. Gated by /api/admin/* middleware (404 + * for non-admins). + * + * Move the path under /api/admin/* so the W0-F1 middleware gate applies. + */ +import { withRequest } from "@/lib/logger"; + +export const runtime = "nodejs"; + +export async function GET(request: Request) { + const log = withRequest(request); + log.warn({ event: "debug_throw" }, "intentional error for Glitchtip smoke test"); + throw new Error("debug_throw_smoke_test: glitchtip wiring verified at " + new Date().toISOString()); +} diff --git a/web/app/api/search/autocomplete/route.ts b/web/app/api/search/autocomplete/route.ts new file mode 100644 index 0000000..3bbf176 --- /dev/null +++ b/web/app/api/search/autocomplete/route.ts @@ -0,0 +1,95 @@ +/** + * /api/search/autocomplete — typo-tolerant prefix search via Meilisearch. + * + * Hits two indexes in parallel and returns a small merged result: + * - documents (title-level matches, used to jump to a doc) + * - chunks (passage-level matches, used for in-doc navigation) + * + * Target latency: sub-30ms inside the docker network. Falls back to empty + * results if Meilisearch is unreachable so the chat / hybrid_search aren't + * blocked. Auth: none — same as /api/search/hybrid; corpus is public. + */ +import { NextResponse } from "next/server"; +import { withRequest } from "@/lib/logger"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const MEILI_URL = process.env.MEILISEARCH_URL || "http://meilisearch:7700"; +const MEILI_KEY = process.env.MEILISEARCH_API_KEY || process.env.MEILI_MASTER_KEY || ""; + +interface DocHit { + doc_id: string; + canonical_title: string; + collection?: string; +} + +interface ChunkHit { + chunk_pk: number; + doc_id: string; + chunk_id: string; + page: number; + type: string; + content_pt?: string; + content_en?: string; + ufo_anomaly?: boolean; +} + +async function meiliSearch(index: string, q: string, limit: number): Promise { + const r = await fetch(`${MEILI_URL}/indexes/${index}/search`, { + method: "POST", + headers: { + "Authorization": `Bearer ${MEILI_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ q, limit, attributesToHighlight: ["canonical_title", "content_pt", "content_en"] }), + signal: AbortSignal.timeout(2000), + }); + if (!r.ok) throw new Error(`meili ${r.status}`); + const data = await r.json(); + return data.hits ?? []; +} + +export async function GET(request: Request) { + const log = withRequest(request); + const url = new URL(request.url); + const q = (url.searchParams.get("q") || "").trim(); + const limit = Math.min(Number(url.searchParams.get("limit") || 8), 20); + + if (q.length < 2) { + return NextResponse.json({ q, documents: [], chunks: [] }); + } + if (!MEILI_KEY) { + log.warn({ event: "autocomplete_unconfigured" }, "MEILI key not set"); + return NextResponse.json({ q, documents: [], chunks: [], reason: "meili_not_configured" }); + } + + const t0 = Date.now(); + const [docs, chunks] = await Promise.all([ + meiliSearch("documents", q, Math.min(limit, 5)).catch(() => []), + meiliSearch("chunks", q, limit).catch(() => []), + ]) as [DocHit[], ChunkHit[]]; + + const dt = Date.now() - t0; + log.info({ event: "autocomplete", q, docs: docs.length, chunks: chunks.length, dt_ms: dt }, "autocomplete done"); + + return NextResponse.json({ + q, + duration_ms: dt, + documents: docs.map((d) => ({ + doc_id: d.doc_id, + title: d.canonical_title, + collection: d.collection, + href: `/d/${d.doc_id}`, + })), + chunks: chunks.map((c) => ({ + chunk_id: c.chunk_id, + doc_id: c.doc_id, + page: c.page, + type: c.type, + excerpt: (c.content_pt || c.content_en || "").slice(0, 180), + ufo_anomaly: !!c.ufo_anomaly, + href: `/d/${c.doc_id}/p${String(c.page).padStart(3, "0")}#${c.chunk_id}`, + })), + }); +} diff --git a/web/app/api/sessions/[id]/messages/route.ts b/web/app/api/sessions/[id]/messages/route.ts index 922dd95..f725086 100644 --- a/web/app/api/sessions/[id]/messages/route.ts +++ b/web/app/api/sessions/[id]/messages/route.ts @@ -18,6 +18,7 @@ import { createClient, isSupabaseConfigured } from "@/lib/supabase/server"; import { readDocument, readPage } from "@/lib/wiki"; import { streamChat } from "@/lib/chat"; import { getLocale } from "@/components/locale-toggle"; +import { withRequest } from "@/lib/logger"; async function gatherContext(docId: string | null, pageId: string | null): Promise { const parts: string[] = []; @@ -129,8 +130,9 @@ Quotes verbatim do documento mantêm idioma original (inglês), narração ao re export async function POST(request: Request, ctx: { params: Promise<{ id: string }> }) { const { id: sessionId } = await ctx.params; const t0 = Date.now(); + const baseLog = withRequest(request).child({ session_id: sessionId.slice(0, 8) }); const log = (stage: string, extra: Record = {}) => - console.log(`[chat ${sessionId.slice(0, 8)}] ${stage}`, { dt: Date.now() - t0, ...extra }); + baseLog.info({ stage, dt_ms: Date.now() - t0, ...extra }, stage); log("POST received"); if (!isSupabaseConfigured()) { diff --git a/web/app/d/[docId]/page.tsx b/web/app/d/[docId]/page.tsx index a827769..9471f76 100644 --- a/web/app/d/[docId]/page.tsx +++ b/web/app/d/[docId]/page.tsx @@ -13,6 +13,7 @@ import { getLocale } from "@/components/locale-toggle"; import { AuthBar } from "@/components/auth-bar"; import { ChatBubble } from "@/components/chat-bubble"; import { DocReadingView } from "@/components/doc-reading-view"; +import { AnomalyHighlights, type AnomalyFlag } from "@/components/anomaly-highlights"; import { MarkdownBody } from "@/components/markdown-body"; export const dynamic = "force-dynamic"; @@ -70,17 +71,31 @@ export default async function DocPage({ .sort((a, b) => b[1] - a[1]) .slice(0, 6); - // Count UFO/cryptid anomalies across chunks - let ufoCount = 0; - let cryptidCount = 0; + // Count UFO/cryptid anomalies across chunks + collect flags for the highlight panel let imageCount = 0; - for (const [, chunks] of byPage) { + const ufoFlags: AnomalyFlag[] = []; + const cryptidFlags: AnomalyFlag[] = []; + for (const [page, chunks] of byPage) { for (const c of chunks) { - if (c.fm.ufo_anomaly_detected) ufoCount++; - if (c.fm.cryptid_anomaly_detected) cryptidCount++; + if (c.fm.ufo_anomaly_detected) + ufoFlags.push({ + chunk_id: c.fm.chunk_id, + page, + type: c.fm.ufo_anomaly_type ?? null, + rationale: c.fm.ufo_anomaly_rationale ?? null, + }); + if (c.fm.cryptid_anomaly_detected) + cryptidFlags.push({ + chunk_id: c.fm.chunk_id, + page, + type: c.fm.cryptid_anomaly_type ?? null, + rationale: c.fm.cryptid_anomaly_rationale ?? null, + }); if (c.fm.type === "image") imageCount++; } } + const ufoCount = ufoFlags.length; + const cryptidCount = cryptidFlags.length; const classification = (doc?.fm.highest_classification as string) ?? "—"; const collection = (doc?.fm.collection as string) ?? "—"; @@ -136,6 +151,8 @@ export default async function DocPage({ )} + + diff --git a/web/app/e/[cls]/[id]/page.tsx b/web/app/e/[cls]/[id]/page.tsx index 12ee8f0..da16713 100644 --- a/web/app/e/[cls]/[id]/page.tsx +++ b/web/app/e/[cls]/[id]/page.tsx @@ -11,6 +11,7 @@ import { ChatBubble } from "@/components/chat-bubble"; import { AuthBar } from "@/components/auth-bar"; import { EntityGraphMini } from "@/components/entity-graph-mini"; import { EntityRelations } from "@/components/entity-relations"; +import { EntityAttributes } from "@/components/entity-attributes"; import { getEntityCore, getEntityMentionsByDoc, @@ -111,6 +112,21 @@ export default async function EntityPage({ const classColor = CLASS_COLOR[folder as EntityClass]; const classBg = CLASS_BG[folder as EntityClass]; + // The generated entity bodies hold only "# Title" + empty "## Description" + // headings — strip headings and see if any real prose remains. + const bodyProse = (wiki?.body ?? "").replace(/^#.*$/gm, "").trim(); + const hasNarrativeProse = bodyProse.length > 20; + // Does the frontmatter carry any displayable description/attribute? + const fm = (wiki?.fm ?? {}) as Record; + const arr = (v: unknown) => Array.isArray(v) && v.length > 0; + const fmHasContent = Boolean( + fm.narrative_summary_pt_br || fm.narrative_summary_en || fm.maneuver_notes || + fm.shape || fm.color || fm.medium || fm.event_class || fm.person_class || + fm.org_class || fm.geo_class || fm.date_start || + arr(fm.countries) || arr(fm.roles) || arr(fm.affiliations) || + arr(fm.primary_location_names) || arr(fm.regions_or_states), + ); + return (
@@ -230,6 +246,9 @@ export default async function EntityPage({
{/* MAIN — narrative + chunks live */}
+ {/* Structured description + attributes from frontmatter */} + {wiki?.fm && } />} + {/* Live chunk previews — most impactful section */} {sampleChunks.length > 0 && (
@@ -283,17 +302,18 @@ export default async function EntityPage({
)} - {/* Narrative body (Haiku stub OK quando rico) */} - {wiki?.body && wiki.body.trim().length > 30 && ( + {/* Narrative body — only when it carries real prose, not just the + empty "## Description" headings the generator leaves behind. */} + {hasNarrativeProse && (

Narrativa

- {wiki.body} + {wiki!.body}
)} - {sampleChunks.length === 0 && (!wiki?.body || wiki.body.trim().length === 0) && ( + {sampleChunks.length === 0 && !hasNarrativeProse && !fmHasContent && (
Entidade ainda sem chunks indexados na DB. Aguarde o indexer terminar.
diff --git a/web/components/anomaly-highlights.tsx b/web/components/anomaly-highlights.tsx new file mode 100644 index 0000000..add7de8 --- /dev/null +++ b/web/components/anomaly-highlights.tsx @@ -0,0 +1,135 @@ +/** + * AnomalyHighlights — prominent UAP / cryptid anomaly panel for the document + * page. The clean reading version is the default body, but the investigative + * "destaque" of every flagged passage must stay visible regardless of which + * view (reading or scan) is active. Identical type+rationale flags are grouped + * and each group links to the per-page scan where the anomaly was detected. + */ +import Link from "next/link"; + +export interface AnomalyFlag { + chunk_id: string; + page: number; + type: string | null; + rationale: string | null; +} + +function clean(v: string | null): string | null { + const s = typeof v === "string" ? v.trim() : ""; + return s && s.toLowerCase() !== "null" ? s : null; +} + +interface Group { + type: string | null; + rationale: string | null; // shown only when the group has a single flag + count: number; + pages: number[]; +} + +// Group by anomaly type so the panel stays a scannable "destaque" overview. +// Per-passage rationale is kept only when a type has exactly one flag; the full +// per-chunk rationale remains available in the "trechos · scan original" view. +function groupFlags(flags: AnomalyFlag[]): Group[] { + const m = new Map(); + for (const f of flags) { + const type = clean(f.type); + const rationale = clean(f.rationale); + const key = type ?? "anomalia"; + const g = m.get(key) ?? { type, rationale, count: 0, pages: [] }; + g.count += 1; + g.rationale = g.count === 1 ? rationale : null; + if (!g.pages.includes(f.page)) g.pages.push(f.page); + m.set(key, g); + } + return Array.from(m.values()) + .map((g) => ({ ...g, pages: g.pages.sort((a, b) => a - b) })) + .sort((a, b) => b.count - a.count || a.pages[0] - b.pages[0]); +} + +function pad(p: number): string { + return String(p).padStart(3, "0"); +} + +function PageChips({ docId, pages }: { docId: string; pages: number[] }) { + const shown = pages.slice(0, 14); + const extra = pages.length - shown.length; + return ( + + {shown.map((p) => ( + + p{p} + + ))} + {extra > 0 && +{extra}} + + ); +} + +export function AnomalyHighlights({ + docId, + ufo, + cryptid, +}: { + docId: string; + ufo: AnomalyFlag[]; + cryptid: AnomalyFlag[]; +}) { + if (ufo.length === 0 && cryptid.length === 0) return null; + const ufoGroups = groupFlags(ufo); + const cryptidGroups = groupFlags(cryptid); + + return ( +
+ {ufo.length > 0 && ( + <> +

+ 🛸 Anomalias UAP destacadas + + ({ufo.length} {ufo.length === 1 ? "trecho" : "trechos"} · {ufoGroups.length}{" "} + {ufoGroups.length === 1 ? "tipo" : "tipos"}) + +

+
    + {ufoGroups.map((g, i) => ( +
  • + 🛸 {g.type ?? "anomalia"} + {g.count > 1 && ( + ×{g.count} + )} + {g.rationale && — {g.rationale}}{" "} + +
  • + ))} +
+ + )} + + {cryptid.length > 0 && ( +
0 ? "mt-4 pt-4 border-t border-[rgba(155,93,229,0.25)]" : ""}> +

+ 👁 Anomalias cryptid destacadas + + ({cryptid.length} {cryptid.length === 1 ? "trecho" : "trechos"}) + +

+
    + {cryptidGroups.map((g, i) => ( +
  • + 👁 {g.type ?? "anomalia"} + {g.count > 1 && ( + ×{g.count} + )} + {g.rationale && — {g.rationale}}{" "} + +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/web/components/entity-attributes.tsx b/web/components/entity-attributes.tsx new file mode 100644 index 0000000..04de337 --- /dev/null +++ b/web/components/entity-attributes.tsx @@ -0,0 +1,164 @@ +/** + * EntityAttributes — renders an entity's descriptive content and structured + * attributes straight from its wiki frontmatter. The generated entity files + * carry their real content in YAML fields (narrative_summary_*, maneuver_notes, + * shape, color, roles, countries, …) while the markdown body holds only empty + * "## Description" headings — so the page must surface the frontmatter. + */ + +type FM = Record; + +const ATTR_LABELS: Record = { + event_class: "Tipo de evento", + date_start: "Início", + date_end: "Fim", + date_confidence: "Confiança da data", + primary_location_names: "Locais", + primary_location_geo_classes: "Classe do local", + geo_class: "Classe geográfica", + countries: "Países", + regions_or_states: "Regiões / estados", + org_class: "Tipo de organização", + person_class: "Tipo de pessoa", + affiliations: "Afiliações", + roles: "Funções / papéis", + shape: "Forma", + color: "Cor", + medium: "Meio", + size_estimate_m: "Tamanho estimado (m)", + altitude_ft: "Altitude (ft)", + speed_kts: "Velocidade (kt)", +}; + +// Order in which attributes are shown (only those present render). +const ATTR_ORDER = [ + "event_class", + "person_class", + "org_class", + "shape", + "color", + "medium", + "size_estimate_m", + "altitude_ft", + "speed_kts", + "date_start", + "date_end", + "date_confidence", + "geo_class", + "countries", + "regions_or_states", + "primary_location_names", + "primary_location_geo_classes", + "affiliations", + "roles", +]; + +function clean(v: unknown): string | null { + const s = typeof v === "string" ? v.trim() : ""; + return s && s.toLowerCase() !== "null" ? s : null; +} + +// Placeholder values that carry no real attribute information — hidden from the +// ATRIBUTOS grid (but never from the free-text description). +const EMPTY_TOKENS = new Set([ + "null", + "none", + "n/a", + "na", + "unknown", + "unidentified", + "undetermined", + "unspecified", + "not specified", + "not stated", + "not applicable", +]); + +function isEmptyToken(s: string): boolean { + return EMPTY_TOKENS.has(s.trim().toLowerCase()); +} + +function fmtValue(v: unknown): string | null { + if (v == null) return null; + if (Array.isArray(v)) { + const items = v + .map((x) => (typeof x === "string" ? x.trim() : String(x))) + .filter((x) => x && !x.startsWith("[[") && !isEmptyToken(x)); + return items.length ? items.join(", ") : null; + } + if (typeof v === "number") return String(v); + const s = clean(v); + return s && !isEmptyToken(s) ? s : null; +} + +export function EntityAttributes({ fm }: { fm: FM }) { + const ptText = clean(fm.narrative_summary_pt_br) ?? clean(fm.description_pt_br); + const enText = clean(fm.narrative_summary_en) ?? clean(fm.description_en); + const notes = clean(fm.maneuver_notes); // source-language only (uap_object) + + const attrs = ATTR_ORDER.map((k) => [k, fmtValue(fm[k])] as const).filter( + ([, v]) => v !== null, + ); + + const hasDescription = Boolean(ptText || enText || notes); + if (!hasDescription && attrs.length === 0) return null; + + return ( +
+ {hasDescription && ( + <> + {ptText && ( +
+

+ Descrição (PT-BR) +

+

{ptText}

+
+ )} + {enText && ( +
+

+ Description (EN) +

+

{enText}

+
+ )} + {notes && !ptText && !enText && ( +
+

+ Descrição · Description +

+

{notes}

+
+ )} + {notes && (ptText || enText) && ( +
+

+ Notas de manobra / aparência +

+

{notes}

+
+ )} + + )} + + {attrs.length > 0 && ( +
+

+ Atributos +

+
+ {attrs.map(([k, v]) => ( +
+
+ {ATTR_LABELS[k] ?? k} +
+
{v}
+
+ ))} +
+
+ )} +
+ ); +} diff --git a/web/components/search-autocomplete.tsx b/web/components/search-autocomplete.tsx new file mode 100644 index 0000000..2b680f8 --- /dev/null +++ b/web/components/search-autocomplete.tsx @@ -0,0 +1,137 @@ +"use client"; + +/** + * SearchAutocomplete — type-as-you-go dropdown on the /search input. + * + * Hits /api/search/autocomplete (Meilisearch) with debounced fetch and renders + * a two-section dropdown: matching documents (jump targets) and matching + * chunks (in-doc passages with excerpt). Sub-30ms target. Keyboard navigation + * via Up/Down + Enter. Esc closes. + */ +import { useEffect, useRef, useState } from "react"; +import Link from "next/link"; + +interface DocSuggestion { + doc_id: string; + title: string; + collection?: string; + href: string; +} +interface ChunkSuggestion { + chunk_id: string; + doc_id: string; + page: number; + type: string; + excerpt: string; + ufo_anomaly: boolean; + href: string; +} + +interface ApiResponse { + q: string; + duration_ms?: number; + documents: DocSuggestion[]; + chunks: ChunkSuggestion[]; +} + +export function SearchAutocomplete({ query, onPick }: { query: string; onPick?: () => void }) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const timer = useRef | null>(null); + const abort = useRef(null); + + useEffect(() => { + const q = query.trim(); + if (q.length < 2) { + setData(null); setOpen(false); return; + } + if (timer.current) clearTimeout(timer.current); + timer.current = setTimeout(async () => { + abort.current?.abort(); + abort.current = new AbortController(); + setLoading(true); + try { + const r = await fetch(`/api/search/autocomplete?q=${encodeURIComponent(q)}`, { + signal: abort.current.signal, + }); + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const j = (await r.json()) as ApiResponse; + setData(j); + setOpen(j.documents.length + j.chunks.length > 0); + } catch (e) { + if ((e as Error).name === "AbortError") return; + setData(null); setOpen(false); + } finally { + setLoading(false); + } + }, 150); + return () => { if (timer.current) clearTimeout(timer.current); }; + }, [query]); + + if (!open || !data) return null; + + return ( +
+
+ + ⚡ autocomplete · {data.documents.length} docs · {data.chunks.length} trechos + + {loading ? "…" : `${data.duration_ms ?? "?"}ms`} +
+ + {data.documents.length > 0 && ( +
+
+ documentos +
+
    + {data.documents.map((d) => ( +
  • + +
    {d.title}
    +
    + {d.doc_id} + {d.collection && · {d.collection}} +
    + +
  • + ))} +
+
+ )} + + {data.chunks.length > 0 && ( +
+
+ trechos +
+
    + {data.chunks.map((c) => ( +
  • + +
    + {c.chunk_id} + p{c.page} + {c.type} + {c.ufo_anomaly && 🛸} + {c.doc_id} +
    +
    {c.excerpt}
    + +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/web/components/search-panel.tsx b/web/components/search-panel.tsx index ab9a6a7..653bfb9 100644 --- a/web/components/search-panel.tsx +++ b/web/components/search-panel.tsx @@ -9,6 +9,7 @@ import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; +import { SearchAutocomplete } from "./search-autocomplete"; interface Hit { chunk_id: string; @@ -94,7 +95,7 @@ export function SearchPanel({ onSubmit={submit} className="space-y-3 mb-8 p-4 border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded" > -
+
@@ -105,6 +106,7 @@ export function SearchPanel({ className="w-full bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-3 py-2 font-mono text-sm text-[#c8d4e6] outline-none" autoFocus /> + setQ("")} />
diff --git a/web/instrumentation.ts b/web/instrumentation.ts new file mode 100644 index 0000000..28141b1 --- /dev/null +++ b/web/instrumentation.ts @@ -0,0 +1,33 @@ +/** + * Next.js instrumentation hook — loads Sentry (Glitchtip) init on server/edge. + * + * https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation + */ +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + await import("./sentry.server.config"); + } + if (process.env.NEXT_RUNTIME === "edge") { + // Edge runtime gets a slimmer init via the same DSN; the SDK auto-detects. + await import("./sentry.server.config"); + } +} + +// Capture unhandled promise rejections in Server Components / API routes and +// forward them through Sentry's hook. Loaded only on the server. +// Forward unhandled errors from Server Components / Route Handlers to Sentry. +// Loose typing so it tracks any captureRequestError signature change in +// @sentry/nextjs — observability code must not block real errors. +export const onRequestError = async ( + err: unknown, + request: Parameters[1], + context: Parameters[2], +) => { + if (process.env.NEXT_RUNTIME !== "nodejs") return; + try { + const { captureRequestError } = await import("@sentry/nextjs"); + await captureRequestError(err, request, context); + } catch { + /* never let observability swallow the original error */ + } +}; diff --git a/web/lib/chat/claude-code.ts b/web/lib/chat/claude-code.ts index ae733d0..5d7cf67 100644 --- a/web/lib/chat/claude-code.ts +++ b/web/lib/chat/claude-code.ts @@ -12,7 +12,11 @@ import { spawn } from "node:child_process"; import type { ChatProvider, ChatRequest, ChatResponse } from "./types"; const MODEL = process.env.CLAUDE_CODE_MODEL || "haiku"; -const TIMEOUT_MS = 90_000; +// W1-TD#30: subprocess timeout is now configurable. Default 90s matches the +// previous hard-coded value. Lower it (e.g. 60s) when the provider should bail +// out of slow generations sooner; raise it (e.g. 180s) when running heavier +// models like opus on long contexts. +const TIMEOUT_MS = Number(process.env.CLAUDE_CODE_TIMEOUT_MS || 90_000); function buildPrompt(req: ChatRequest): string { // Single-shot prompt: collapse history into a structured transcript. diff --git a/web/lib/chat/openrouter.ts b/web/lib/chat/openrouter.ts index 690e79f..ba4ad55 100644 --- a/web/lib/chat/openrouter.ts +++ b/web/lib/chat/openrouter.ts @@ -23,6 +23,105 @@ const PRIMARY = process.env.OPENROUTER_MODEL || "deepseek/deepseek-v4-flash:free const FALLBACK = process.env.OPENROUTER_FALLBACK_MODEL || "nvidia/nemotron-3-super-120b-a12b:free"; const ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"; +// W1-TD#23: retry + circuit breaker for OpenRouter free-tier flakiness. +// Transient errors (429/502/503/504/network) are retried up to RETRY_MAX times +// with exponential backoff. Repeated PRIMARY failures within CB_WINDOW_MS +// trip an in-memory circuit breaker that promotes FALLBACK as the active +// model for CB_COOLDOWN_MS — protecting the chat from a single bad model. +const RETRY_MAX = Number(process.env.OPENROUTER_RETRY_MAX || 2); +const RETRY_BASE_MS = Number(process.env.OPENROUTER_RETRY_BASE_MS || 400); +const CB_WINDOW_MS = Number(process.env.OPENROUTER_CB_WINDOW_MS || 60_000); +const CB_THRESHOLD = Number(process.env.OPENROUTER_CB_THRESHOLD || 3); +const CB_COOLDOWN_MS = Number(process.env.OPENROUTER_CB_COOLDOWN_MS || 120_000); + +const RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]); + +interface ModelBreaker { failures: number[]; openedAt: number | null } +const breakers = new Map(); + +function breakerFor(model: string): ModelBreaker { + let b = breakers.get(model); + if (!b) { b = { failures: [], openedAt: null }; breakers.set(model, b); } + return b; +} + +function isCircuitOpen(model: string): boolean { + const b = breakerFor(model); + if (!b.openedAt) return false; + if (Date.now() - b.openedAt > CB_COOLDOWN_MS) { + // Half-open: clear and let the next call probe the upstream. + b.openedAt = null; b.failures = []; + return false; + } + return true; +} + +function recordFailure(model: string): void { + const b = breakerFor(model); + const now = Date.now(); + b.failures = b.failures.filter((t) => now - t < CB_WINDOW_MS); + b.failures.push(now); + if (b.failures.length >= CB_THRESHOLD) b.openedAt = now; +} + +function recordSuccess(model: string): void { + const b = breakerFor(model); + b.failures = []; b.openedAt = null; +} + +/** Pick the active model honoring an open circuit on PRIMARY. */ +function pickModel(preferred: string): string { + if (preferred === PRIMARY && isCircuitOpen(PRIMARY)) return FALLBACK; + return preferred; +} + +/** Fetch wrapper with retry + breaker accounting. */ +async function fetchOpenRouter( + body: Record, + preferredModel: string, +): Promise<{ res: Response; model: string }> { + const model = pickModel(preferredModel); + body.model = model; + + let lastErr: unknown; + for (let attempt = 0; attempt <= RETRY_MAX; attempt++) { + try { + const res = await fetch(ENDPOINT, { + method: "POST", + headers: headers(), + body: JSON.stringify(body), + }); + if (res.ok) { + recordSuccess(model); + return { res, model }; + } + if (!RETRYABLE_STATUSES.has(res.status)) { + const txt = await res.text(); + const err = new Error(`openrouter HTTP ${res.status}: ${txt.slice(0, 300)}`); + if (res.status === 429 || res.status === 402) { + (err as Error & { isRateLimit?: boolean }).isRateLimit = true; + } + recordFailure(model); + throw err; + } + // Retryable — wait with exponential backoff, honor Retry-After if present. + const ra = Number(res.headers.get("retry-after")); + const waitMs = Number.isFinite(ra) && ra > 0 + ? ra * 1000 + : RETRY_BASE_MS * Math.pow(2, attempt); + await new Promise((r) => setTimeout(r, waitMs)); + lastErr = new Error(`openrouter HTTP ${res.status} (attempt ${attempt + 1}/${RETRY_MAX + 1})`); + } catch (e) { + // Network/abort — also retry up to RETRY_MAX. + lastErr = e; + if (attempt >= RETRY_MAX) break; + await new Promise((r) => setTimeout(r, RETRY_BASE_MS * Math.pow(2, attempt))); + } + } + recordFailure(model); + throw lastErr instanceof Error ? lastErr : new Error(String(lastErr)); +} + type OAMsg = | { role: "system" | "user"; content: string } | { role: "assistant"; content?: string | null; tool_calls?: OAToolCall[] } @@ -74,35 +173,26 @@ export interface SendOnceReq { } /** Non-streaming single shot — used by claude-code fallback path and tests. */ -export async function sendOnce(req: SendOnceReq, model = PRIMARY): Promise<{ +export async function sendOnce(req: SendOnceReq, preferredModel = PRIMARY): Promise<{ content: string; model: string; tokensIn?: number; tokensOut?: number; }> { - const body = { - model, + const body: Record = { messages: [ { role: "system", content: req.system }, ...req.messages.slice(-20), ], max_tokens: req.maxTokens ?? 1024, }; - const res = await fetch(ENDPOINT, { - method: "POST", - headers: headers(), - body: JSON.stringify(body), - }); - if (!res.ok) { - const txt = await res.text(); - const err = new Error(`openrouter HTTP ${res.status}: ${txt.slice(0, 300)}`); - if (res.status === 429 || res.status === 402) { - (err as Error & { isRateLimit?: boolean }).isRateLimit = true; - } - throw err; - } + const { res, model } = await fetchOpenRouter(body, preferredModel); const data = await res.json(); - if (data.error) throw new Error(`openrouter error: ${data.error.message}`); + if (data.error) { + recordFailure(model); + throw new Error(`openrouter error: ${data.error.message}`); + } + recordSuccess(model); return { content: data.choices?.[0]?.message?.content ?? "", model: data.model ?? model, @@ -336,12 +426,11 @@ export async function streamWithTools( async function openrouterStreamCall( messages: OAMsg[], - model: string, + preferredModel: string, opts: { withTools?: boolean } = {}, ): Promise { const withTools = opts.withTools !== false; const body: Record = { - model, messages, stream: true, max_tokens: 1024, @@ -350,19 +439,7 @@ async function openrouterStreamCall( body.tools = TOOL_DEFINITIONS; body.tool_choice = "auto"; } - const res = await fetch(ENDPOINT, { - method: "POST", - headers: headers(), - body: JSON.stringify(body), - }); - if (!res.ok) { - const txt = await res.text(); - const err = new Error(`openrouter HTTP ${res.status}: ${txt.slice(0, 300)}`); - if (res.status === 429 || res.status === 402) { - (err as Error & { isRateLimit?: boolean }).isRateLimit = true; - } - throw err; - } + const { res } = await fetchOpenRouter(body, preferredModel); return res; } diff --git a/web/lib/logger.ts b/web/lib/logger.ts new file mode 100644 index 0000000..e12da10 --- /dev/null +++ b/web/lib/logger.ts @@ -0,0 +1,77 @@ +/** + * Structured logger — pino with JSON output in production, pretty in dev. + * + * Use as: + * import { log, withRequest } from "@/lib/logger"; + * log.info({ doc_id, page }, "rendering page"); + * log.error({ err }, "embed-service down"); + * + * For request-scoped logging: + * const reqLog = withRequest(request); + * reqLog.info({ duration_ms: dt }, "hybrid_search done"); + * + * Edge runtime falls back to a console adapter (pino requires node). + */ +import pino from "pino"; + +// Edge runtime doesn't support pino's worker thread; detect and fall back. +const isEdge = typeof process === "undefined" || process.env.NEXT_RUNTIME === "edge"; + +function build(): pino.Logger { + if (isEdge) { + // Minimal adapter so middleware can call log.* without crashing. + const noop = () => undefined; + return { + info: (o: unknown, m?: string) => console.log(JSON.stringify({ level: "info", msg: m, ...(typeof o === "object" ? o : { v: o }) })), + warn: (o: unknown, m?: string) => console.warn(JSON.stringify({ level: "warn", msg: m, ...(typeof o === "object" ? o : { v: o }) })), + error: (o: unknown, m?: string) => console.error(JSON.stringify({ level: "error", msg: m, ...(typeof o === "object" ? o : { v: o }) })), + debug: noop, + trace: noop, + fatal: (o: unknown, m?: string) => console.error(JSON.stringify({ level: "fatal", msg: m, ...(typeof o === "object" ? o : { v: o }) })), + child: () => build(), + } as unknown as pino.Logger; + } + return pino({ + level: process.env.LOG_LEVEL || "info", + base: { + app: "disclosure-web", + env: process.env.NODE_ENV || "development", + }, + timestamp: pino.stdTimeFunctions.isoTime, + // Production: NDJSON (one JSON per line). Dev: pretty-printed. + transport: process.env.NODE_ENV === "production" ? undefined : { + target: "pino-pretty", + options: { colorize: true, translateTime: "SYS:HH:MM:ss.l" }, + }, + }); +} + +export const log: pino.Logger = build(); + +/** Create a child logger bound to a request's correlation id. */ +export function withRequest(req: Request | { headers: Headers }): pino.Logger { + const id = req.headers.get("x-correlation-id") || + req.headers.get("x-request-id") || + cryptoRandomId(); + return log.child({ correlation_id: id }); +} + +/** Get-or-mint a correlation id for a request. */ +export function correlationId(req: Request | { headers: Headers }): string { + return req.headers.get("x-correlation-id") || + req.headers.get("x-request-id") || + cryptoRandomId(); +} + +function cryptoRandomId(): string { + // 16 hex chars — short enough for logs, enough entropy for non-security uses. + // Both edge runtime and Node 19+ expose globalThis.crypto; older Node falls + // back to Math.random (acceptable: this is correlation, not security). + const g = globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => void } }; + if (g.crypto?.getRandomValues) { + const buf = new Uint8Array(8); + g.crypto.getRandomValues(buf); + return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join(""); + } + return Math.random().toString(36).slice(2, 18); +} diff --git a/web/middleware.ts b/web/middleware.ts index fed0caf..c4ec030 100644 --- a/web/middleware.ts +++ b/web/middleware.ts @@ -6,12 +6,17 @@ */ import { NextResponse, type NextRequest } from "next/server"; import { createServerClient, type CookieOptions } from "@supabase/ssr"; +import { log, correlationId } from "@/lib/logger"; export async function middleware(request: NextRequest) { + const t0 = Date.now(); const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + const reqId = correlationId(request); let response = NextResponse.next({ request }); + // Stamp every response so downstream handlers and the client see the same id. + response.headers.set("x-correlation-id", reqId); if (!url || !key) { // Supabase not configured — skip auth refresh entirely @@ -34,10 +39,11 @@ export async function middleware(request: NextRequest) { // Trigger refresh (silently if token still valid) const { data: { user } } = await supabase.auth.getUser(); - // Gate /admin/* by role. Non-admin (including anonymous) gets the public - // 404, not a redirect — we don't want to leak the existence of the route. + // Gate /admin/* AND /api/admin/* by role. Non-admin (including anonymous) + // gets a public 404, not a redirect — we don't want to leak the existence + // of the route. (Audit W0-F1 — fechado 2026-05-23.) const pathname = request.nextUrl.pathname; - if (pathname.startsWith("/admin")) { + if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) { if (!user) { return new NextResponse("Not Found", { status: 404 }); } @@ -51,6 +57,22 @@ export async function middleware(request: NextRequest) { } } + // Log API requests with correlation id + timing. Skip noisy paths (assets, + // crops) and prefer one structured line per request so Glitchtip / log + // aggregators can correlate. + if (pathname.startsWith("/api/") && !pathname.startsWith("/api/static") && !pathname.startsWith("/api/crop")) { + log.info( + { + event: "http_request", + method: request.method, + path: pathname, + correlation_id: reqId, + duration_ms: Date.now() - t0, + }, + `${request.method} ${pathname}`, + ); + } + return response; } diff --git a/web/package-lock.json b/web/package-lock.json index e2e2265..edf7a92 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ "@radix-ui/react-tooltip": "^1.1.0", "@react-sigma/core": "^5.0.0", "@react-sigma/layout-forceatlas2": "^5.0.0", + "@sentry/nextjs": "^10.53.1", "@supabase/ssr": "^0.10.3", "@supabase/supabase-js": "^2.105.4", "framer-motion": "^11.11.0", @@ -22,6 +23,7 @@ "lucide-react": "^0.460.0", "next": "^15.1.0", "pg": "^8.13.1", + "pino": "^10.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-force-graph-2d": "^1.27.0", @@ -165,6 +167,203 @@ } } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -174,6 +373,51 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -626,6 +870,72 @@ "node": ">=18" } }, + "node_modules/@fastify/otel": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@fastify/otel/-/otel-0.18.0.tgz", + "integrity": "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.212.0", + "@opentelemetry/semantic-conventions": "^1.28.0", + "minimatch": "^10.2.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@fastify/otel/node_modules/@opentelemetry/api-logs": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.212.0.tgz", + "integrity": "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@fastify/otel/node_modules/@opentelemetry/instrumentation": { + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", + "integrity": "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.212.0", + "import-in-the-middle": "^2.0.6", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@fastify/otel/node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -1134,18 +1444,26 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1155,14 +1473,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1341,6 +1657,500 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", + "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.61.0.tgz", + "integrity": "sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.57.0.tgz", + "integrity": "sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.31.0.tgz", + "integrity": "sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.33.0.tgz", + "integrity": "sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.57.0.tgz", + "integrity": "sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.62.0.tgz", + "integrity": "sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.60.0.tgz", + "integrity": "sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.214.0.tgz", + "integrity": "sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/instrumentation": "0.214.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz", + "integrity": "sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.58.0.tgz", + "integrity": "sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.62.0.tgz", + "integrity": "sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.58.0.tgz", + "integrity": "sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.67.0.tgz", + "integrity": "sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.60.0.tgz", + "integrity": "sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.60.0.tgz", + "integrity": "sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.60.0.tgz", + "integrity": "sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz", + "integrity": "sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz", + "integrity": "sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@prisma/instrumentation": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.6.0.tgz", + "integrity": "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.207.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz", + "integrity": "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz", + "integrity": "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.207.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@prisma/instrumentation/node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4236,6 +5046,842 @@ "graphology-layout-forceatlas2": "^0.10.1" } }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.53.1.tgz", + "integrity": "sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.53.1.tgz", + "integrity": "sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.53.1.tgz", + "integrity": "sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.53.1", + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.53.1.tgz", + "integrity": "sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.53.1", + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.3.0.tgz", + "integrity": "sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.53.1.tgz", + "integrity": "sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.53.1", + "@sentry-internal/feedback": "10.53.1", + "@sentry-internal/replay": "10.53.1", + "@sentry-internal/replay-canvas": "10.53.1", + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/bundler-plugin-core": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.3.0.tgz", + "integrity": "sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "5.3.0", + "@sentry/cli": "^2.58.5", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^13.0.6", + "magic-string": "~0.30.8" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/cli": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.6.tgz", + "integrity": "sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==", + "hasInstallScript": true, + "license": "FSL-1.1-MIT", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.58.6", + "@sentry/cli-linux-arm": "2.58.6", + "@sentry/cli-linux-arm64": "2.58.6", + "@sentry/cli-linux-i686": "2.58.6", + "@sentry/cli-linux-x64": "2.58.6", + "@sentry/cli-win32-arm64": "2.58.6", + "@sentry/cli-win32-i686": "2.58.6", + "@sentry/cli-win32-x64": "2.58.6" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.6.tgz", + "integrity": "sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==", + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.6.tgz", + "integrity": "sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==", + "cpu": [ + "arm" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.6.tgz", + "integrity": "sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.6.tgz", + "integrity": "sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.6.tgz", + "integrity": "sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.6.tgz", + "integrity": "sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.6.tgz", + "integrity": "sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.6.tgz", + "integrity": "sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/core": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz", + "integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/nextjs": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.53.1.tgz", + "integrity": "sha512-pkwqrpAG//LtW5W1Odud0PLLT+rnjDjodUEbScULHVaZE6/Gt+WGBMZmtzpNM+UwhsN19/4PyO7ocLTx/IFrkQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@rollup/plugin-commonjs": "28.0.1", + "@sentry-internal/browser-utils": "10.53.1", + "@sentry/bundler-plugin-core": "^5.3.0", + "@sentry/core": "10.53.1", + "@sentry/node": "10.53.1", + "@sentry/opentelemetry": "10.53.1", + "@sentry/react": "10.53.1", + "@sentry/vercel-edge": "10.53.1", + "@sentry/webpack-plugin": "^5.3.0", + "rollup": "^4.60.3", + "stacktrace-parser": "^0.1.11" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0" + } + }, + "node_modules/@sentry/node": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.53.1.tgz", + "integrity": "sha512-rxHVil0tJAmz+keFcZCj1LaUdgdkK66E/l0gqh2p1209PNCGoD3lnClFr6vusy1aF3zF8O9JPtuMEJzXOKhs+w==", + "license": "MIT", + "dependencies": { + "@fastify/otel": "0.18.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/core": "^2.6.1", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/instrumentation-amqplib": "0.61.0", + "@opentelemetry/instrumentation-connect": "0.57.0", + "@opentelemetry/instrumentation-dataloader": "0.31.0", + "@opentelemetry/instrumentation-fs": "0.33.0", + "@opentelemetry/instrumentation-generic-pool": "0.57.0", + "@opentelemetry/instrumentation-graphql": "0.62.0", + "@opentelemetry/instrumentation-hapi": "0.60.0", + "@opentelemetry/instrumentation-http": "0.214.0", + "@opentelemetry/instrumentation-kafkajs": "0.23.0", + "@opentelemetry/instrumentation-knex": "0.58.0", + "@opentelemetry/instrumentation-koa": "0.62.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", + "@opentelemetry/instrumentation-mongodb": "0.67.0", + "@opentelemetry/instrumentation-mongoose": "0.60.0", + "@opentelemetry/instrumentation-mysql": "0.60.0", + "@opentelemetry/instrumentation-mysql2": "0.60.0", + "@opentelemetry/instrumentation-pg": "0.66.0", + "@opentelemetry/instrumentation-tedious": "0.33.0", + "@opentelemetry/sdk-trace-base": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@prisma/instrumentation": "7.6.0", + "@sentry/core": "10.53.1", + "@sentry/node-core": "10.53.1", + "@sentry/opentelemetry": "10.53.1", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.53.1.tgz", + "integrity": "sha512-iH7SMcM/7jPbN+t7+7ussQOiIqI4BMOGt4VYWlV71/z7k0pY+YPaSvlfVkNXfISiDzFAKv0ecCY3BmsLMu1xDQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.53.1", + "@sentry/opentelemetry": "10.53.1", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/semantic-conventions": "^1.39.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "@opentelemetry/semantic-conventions": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.53.1.tgz", + "integrity": "sha512-Zok6UXla0mFOjd1YnVb1TZtQNOry9v93fXUqx8jmDaygwWM2BwvP8rBQabLz0/OZXo8+35oge+Vmw+QY5aesnA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/semantic-conventions": "^1.39.0" + } + }, + "node_modules/@sentry/react": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.53.1.tgz", + "integrity": "sha512-lrwNq5T/zW84l60894TpKHPcvFuc1I/Hnohecc0TfYVpIcYYuw2orCHoU4v4wgkFaJUpegVetbgdOphViyLVjA==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.53.1", + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/vercel-edge": { + "version": "10.53.1", + "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.53.1.tgz", + "integrity": "sha512-waIOoLfhi1V3xEBJ1s1hpmvvgvcorYfsfm7fQGye0PgVjcBsZUqz32N5iEwkZ2Gz3n4ZOQYibDUqARJi9tOBcw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/resources": "^2.6.1", + "@sentry/core": "10.53.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/webpack-plugin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-5.3.0.tgz", + "integrity": "sha512-i3OQUrS0FZlXLgq57RIKDp+vHHzuvYKPCKewAPXULWKMsBXFGhP6veGRQ+6To/pmZkkXjEX5ofVNDy9C3jEPKQ==", + "license": "MIT", + "dependencies": { + "@sentry/bundler-plugin-core": "5.3.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "webpack": ">=5.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4353,6 +5999,15 @@ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", "license": "MIT" }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -4401,11 +6056,19 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "22.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4415,7 +6078,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -4423,6 +6085,15 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/pg-pool": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", + "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -4443,6 +6114,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -4464,6 +6144,39 @@ "node": ">=12" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -4545,6 +6258,15 @@ } } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", @@ -4592,11 +6314,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.29", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4628,6 +6358,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -4645,7 +6387,6 @@ "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4805,6 +6546,12 @@ "node": ">= 6" } }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4872,6 +6619,18 @@ "node": ">= 6" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -5202,11 +6961,22 @@ "dev": true, "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.355", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.355.tgz", "integrity": "sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==", - "dev": true, "license": "ISC" }, "node_modules/es-errors": { @@ -5265,7 +7035,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5306,6 +7075,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -5373,6 +7148,23 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5386,6 +7178,22 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/float-tooltip": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", @@ -5426,6 +7234,12 @@ "node": ">=12" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -5471,7 +7285,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5492,6 +7305,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5501,6 +7323,23 @@ "node": ">=6" } }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5633,6 +7472,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -5642,6 +7494,21 @@ "node": ">=20.0.0" } }, + "node_modules/import-in-the-middle": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", + "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/index-array-by": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", @@ -5799,6 +7666,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/jerrypick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", @@ -5837,6 +7719,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/kapsule": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", @@ -5878,6 +7784,21 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -5906,6 +7827,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/lucide-react": { "version": "0.460.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", @@ -5915,6 +7845,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -6947,6 +8886,36 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -7502,11 +9471,30 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.44", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", - "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -7544,6 +9532,45 @@ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -7569,6 +9596,15 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -7576,6 +9612,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", @@ -7694,6 +9755,43 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -7935,6 +10033,31 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7956,6 +10079,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7977,6 +10106,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/radix-ui": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", @@ -8287,6 +10422,15 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -8373,6 +10517,19 @@ "node": ">=0.10" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -8406,6 +10563,56 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8436,6 +10643,15 @@ "integrity": "sha512-+R0IHHjvghT5O8bc8itf9AoS9MvzhUcD0p+hNINLgyEuFQJug3wt3ZuhLFZFG3bUzHi8UfQED4p6J3/Ft9oCtg==", "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -8541,6 +10757,15 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8575,6 +10800,18 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -8736,6 +10973,24 @@ "node": ">=0.8" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -8759,24 +11014,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -8803,6 +11040,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -8855,6 +11098,15 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -8873,7 +11125,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -8967,7 +11218,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9135,6 +11385,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9144,6 +11425,24 @@ "node": ">=0.4" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/web/package.json b/web/package.json index c26ec43..0f5112d 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-tooltip": "^1.1.0", "@react-sigma/core": "^5.0.0", "@react-sigma/layout-forceatlas2": "^5.0.0", + "@sentry/nextjs": "^10.53.1", "@supabase/ssr": "^0.10.3", "@supabase/supabase-js": "^2.105.4", "framer-motion": "^11.11.0", @@ -24,6 +25,7 @@ "lucide-react": "^0.460.0", "next": "^15.1.0", "pg": "^8.13.1", + "pino": "^10.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-force-graph-2d": "^1.27.0", diff --git a/web/sentry.client.config.ts b/web/sentry.client.config.ts new file mode 100644 index 0000000..9b27091 --- /dev/null +++ b/web/sentry.client.config.ts @@ -0,0 +1,17 @@ +/** + * Sentry (Glitchtip-compatible) client-side init. Loaded by Next.js + * automatically when @sentry/nextjs is installed. + */ +import * as Sentry from "@sentry/nextjs"; + +const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN; +if (dsn) { + Sentry.init({ + dsn, + environment: process.env.NODE_ENV || "development", + tracesSampleRate: 0, + sendDefaultPii: false, + // Capture unhandled promise rejections + JS errors. Glitchtip community + // ignores everything below `error` severity by default. + }); +} diff --git a/web/sentry.server.config.ts b/web/sentry.server.config.ts new file mode 100644 index 0000000..f13e692 --- /dev/null +++ b/web/sentry.server.config.ts @@ -0,0 +1,21 @@ +/** + * Sentry (Glitchtip-compatible) server-side init. + * + * DSN must point to Glitchtip — we never send to sentry.io. See + * SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN in docker-compose.yml. If unset, the SDK + * is loaded but no events ship — safe for local dev. + */ +import * as Sentry from "@sentry/nextjs"; + +const dsn = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; +if (dsn) { + Sentry.init({ + dsn, + environment: process.env.NODE_ENV || "development", + release: process.env.SENTRY_RELEASE, + tracesSampleRate: 0, // Glitchtip community doesn't support performance traces + sendDefaultPii: false, + // Make sure events land on Glitchtip's tunnel-friendly DSN host, not + // sentry.io. The SDK already infers from DSN; this is just defensive. + }); +}