commit 62f20f88432ba7567329229ef9b607cbfcefb35b Author: Заид Омар Медхат | Zaid Omar Medhat Date: Fri Jul 10 13:31:12 2026 +0500 init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..91caf09 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +**/node_modules +**/dist +**/.turbo +**/*.tsbuildinfo +**/.env +**/.env.* +!**/.env.example +.git +**/.expo +packages/desktop/src-tauri/target diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..62d5244 --- /dev/null +++ b/.env.example @@ -0,0 +1,58 @@ +# ============================================================================ +# Altricade Messenger — environment template. +# Copy to `.env` (gitignored) and replace every value below. +# The values here are CLEARLY-FAKE dev placeholders — NEVER use in production. +# ============================================================================ + +# --- General --- +NODE_ENV=development + +# --- Backend (Fastify) --- +BACKEND_PORT=4000 +# Public base URL nginx exposes the API under (used for CORS, links). +PUBLIC_API_URL=http://localhost:8080 + +# --- App auth tokens (Phase 1) --- +# Signing key for short-lived access JWTs. Generate: `openssl rand -base64 48` +JWT_ACCESS_SECRET=dev-CHANGE-ME-access-secret-not-for-prod +ACCESS_TOKEN_TTL=15m +REFRESH_TOKEN_TTL=30d + +# --- Centrifugo connection token (SEPARATE from app tokens) --- +# HMAC secret SHARED between backend (mints) and Centrifugo (verifies). +# Generate: `openssl rand -base64 48` +CENTRIFUGO_TOKEN_HMAC_SECRET=dev-CHANGE-ME-centrifugo-hmac-not-for-prod +# API key the backend uses to call Centrifugo's server HTTP API (publish, etc). +CENTRIFUGO_API_KEY=dev-CHANGE-ME-centrifugo-api-key +# Internal URL of the Centrifugo HTTP API (service name on the compose network). +CENTRIFUGO_API_URL=http://centrifugo:8000/api + +# --- Postgres --- +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=altricade +POSTGRES_USER=altricade +POSTGRES_PASSWORD=dev-CHANGE-ME-postgres-password +# Full URL derived from the above (used by backend + migrations). +DATABASE_URL=postgres://altricade:dev-CHANGE-ME-postgres-password@postgres:5432/altricade + +# --- Redis (Centrifugo scaling + history/recovery; auth rate-limiting later) --- +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_URL=redis://redis:6379 + +# --- MinIO (object storage: media messages + avatars) --- +MINIO_ROOT_USER=altricade +MINIO_ROOT_PASSWORD=dev-CHANGE-ME-minio-password +MINIO_ENDPOINT=minio +MINIO_PORT=9000 +MINIO_USE_SSL=false +MINIO_BUCKET_MEDIA=media +MINIO_BUCKET_AVATARS=avatars + +# --- nginx (public entrypoint) --- +NGINX_HTTP_PORT=8080 + +# --- Web (Vite) --- +# Base URL the web client points at for REST + realtime. +VITE_API_URL=http://localhost:8080 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f293046 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# dependencies +node_modules/ +.pnp.* + +# builds +dist/ +build/ +*.tsbuildinfo + +# turbo +.turbo/ + +# env / secrets — never commit real secrets +.env +.env.* +!.env.example + +# logs +*.log +npm-debug.log* +pnpm-debug.log* + +# editor / os +.DS_Store +.idea/ +.vscode/* +!.vscode/extensions.json + +# expo / mobile +.expo/ +*.orig.* + +# tauri / desktop +packages/desktop/src-tauri/target/ + +# docker volumes (if ever bind-mounted locally) +.data/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0ce2128 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..709cce2 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Altricade Messenger + +A self-hosted, cross-platform realtime messenger (chat + later calls). TypeScript +everywhere, pnpm monorepo. See `specs.md` for the full architecture brief. + +> **Status:** Phase 0 — skeleton + Docker. The full backend stack boots with one +> command; no product features yet. Phases are built one at a time (see `specs.md` §8). + +## Architecture in one paragraph + +Centrifugo is a **dumb, fast pipe**; the **backend is the brain**. Clients **send** +over REST and **receive** over the Centrifugo WebSocket. The backend authorizes, +persists to Postgres (source of truth), then publishes to Centrifugo which fans out +to subscribers. Media lives in MinIO; only references travel through channels. Redis +backs Centrifugo history/recovery and clustering. + +## Monorepo layout + +``` +packages/ + core/ # THE KEYSTONE — pure TS, imported by all (types, events, channels, ...) + backend/ # Fastify service (Docker) — imports core + web/ # React + Vite — imports core (Feature-Sliced Design) + mobile/ # React Native + Expo (Phase 7) — placeholder + desktop/ # Tauri shell over web build (Phase 7) — placeholder +infra/ # centrifugo, nginx, postgres init +docker-compose*.yml +``` + +`core` is consumed **as source** across packages, so a type change surfaces as an +immediate compile error everywhere. Builds go through bundlers (Vite for web, tsup +for backend); `tsc` is used only for type-checking. + +## Prerequisites + +- Node.js ≥ 22 and pnpm (via `corepack enable`) +- Docker + Docker Compose + +## Local development + +```bash +# 1. Install the workspace +pnpm install + +# 2. Configure env (copy the template, adjust if you like — dev defaults work) +cp .env.example .env + +# 3. Bring up the whole backend stack (Postgres, Redis, MinIO, Centrifugo, +# backend, nginx) with hot reload. The dev override is auto-loaded. +docker compose up --build + +# 4. Run the web client (separate terminal) +pnpm --filter @altricade/web dev +``` + +Endpoints (via the dev override): + +- Gateway (nginx): http://localhost:8080 +- API health: http://localhost:8080/api/health → `{ "status": "ok" }` +- API readiness: http://localhost:8080/api/ready → 200 only when Postgres + Redis + MinIO are reachable +- Web client: http://localhost:5173 +- MinIO console: http://localhost:9001 + +## Quality gates (enforced mechanically — a violation fails the build) + +```bash +pnpm typecheck # strict TypeScript across all packages +pnpm lint # ESLint: no any, no type assertions, no non-null !, FSD import boundaries +pnpm build # bundle backend + build web +pnpm format # prettier +``` + +Standards: strict TS, no `any`, no `as` type assertions, no `!`, functional +iteration, and strict **Feature-Sliced Design** import boundaries (frontend) plus +layered backend modules. Dependencies are always added via `pnpm add` (never +hand-pinned) at their latest trusted versions. + +## Deploy (production) + +```bash +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +``` + +Only nginx is published to the host; all other services stay on the private network. +Secrets come from the environment — never commit a real `.env`. diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..03c4153 --- /dev/null +++ b/claude.md @@ -0,0 +1,192 @@ +# Project Spec — Realtime Chat & Calls App + +> This document is a brief for Claude Code (plan mode). Read it fully, then propose a +> plan before writing any code. Build the skeleton first, then implement part by part +> with check-ins. Do NOT scaffold the whole thing in one pass. See "How to work" at the +> bottom — it is as important as the architecture. + +--- + +## 1. What we are building + +A self-hosted, cross-platform messaging app with: + +- **1:1 direct messages and group messages** (Telegram-shaped). +- **Media messages**: voice notes, video messages, images, videos (files). +- **Voice/video calls with screen share** — group conferences and 1:1. **This is a LATER + phase. Do not scaffold call/WebRTC code until the chat core is working and I say so.** + +Targets: web, iOS, Android, and desktop (macOS + Windows). + +## 2. Core mental model (read this before anything else) + +The realtime transport (**Centrifugo**) is a **dumb, fast pipe**, NOT the chat server. +The **backend is the brain**. The message-send flow is deliberately: + +1. Client POSTs "send message" to the backend REST API (the client's realtime socket is + effectively **receive-only**). +2. Backend authorizes, writes the message to Postgres (source of truth), then calls + Centrifugo's HTTP API to publish into the channel. +3. Centrifugo fans it out to subscribers. + +Do not let clients publish directly through Centrifugo. All business logic, validation, +and persistence live in the backend. + +### Channel model + +- `room:` — one channel per room; all members subscribe. +- `user:` — one **personal channel** per user. DMs are delivered by publishing the + message into BOTH participants' personal channels; the client routes by `conversationId` + in the payload. This scales to one subscription per user regardless of conversation count, + and doubles as the notification / unread / "added to room" channel. + +### Two auth gates (keep them separate) + +- **Connection auth**: backend mints a JWT (shared secret with Centrifugo) at login; + Centrifugo verifies the signature — no backend call per connection. +- **Subscription auth**: use the **subscribe-proxy** pattern — Centrifugo calls a backend + endpoint on each subscribe to check Postgres membership ("can user U join channel C?"). + This reflects current DB state (kicked users lose access immediately). Personal-channel + subscriptions are authorized by matching the token's user id. + +### Boundaries to respect + +- Centrifugo history is a **short recovery buffer** (reconnect replay), NOT the message + store. Load conversation history from Postgres via REST. +- **Server-assigned message IDs + ordering** and **client-side dedupe** are required from + day one (messages can arrive both live and via history load). These are painful to + retrofit. +- Media (voice notes / video messages / images) upload to object storage (MinIO) via the + backend (presigned URLs); only a **reference** (URL + metadata) is published through the + channel. Never push binary through Centrifugo. + +## 3. Stack (decided — do not re-litigate) + +- **Language everywhere:** TypeScript. +- **Monorepo:** single repo, pnpm workspaces. (NOT separate repos — shared code is imported + by source so type changes surface as immediate compile errors.) +- **Backend framework:** Fastify. (NestJS was considered and rejected: too much + ceremony/DI for a small team and a realtime/event-shaped workload.) +- **Validation:** JSON Schema via Fastify's built-in support, sourced from the shared package. +- **Realtime transport:** Centrifugo (self-hosted, external service). +- **DB:** Postgres 16 (source of truth). **Redis** for Centrifugo scaling + history/recovery. +- **Object storage:** MinIO (S3-compatible). +- **Web frontend:** React + TypeScript + Vite. +- **Mobile frontend:** React Native + Expo + TypeScript. +- **Desktop:** thin Tauri (preferred) or Electron shell wrapping the web build. +- **Calls (LATER phase):** LiveKit (SFU) + coturn (TURN over TLS/443). Not now. + +## 4. Monorepo layout + +``` +/ +├── package.json # pnpm workspace root +├── pnpm-workspace.yaml +├── tsconfig.base.json +├── packages/ +│ ├── core/ # THE KEYSTONE — platform-agnostic, imported by all +│ │ ├── src/ +│ │ │ ├── types/ # message/event/API payload types +│ │ │ ├── schemas/ # JSON Schemas (backend validates, clients type off these) +│ │ │ ├── channels/ # channel-name builders: room:, user: +│ │ │ ├── realtime/ # Centrifugo SDK wrapper (UI-agnostic) +│ │ │ ├── api/ # typed API client functions +│ │ │ └── messages/ # dedupe + ordering logic +│ │ └── package.json +│ ├── backend/ # Fastify service; imports core +│ │ ├── src/ +│ │ │ ├── routes/ # auth, messages, rooms, conversations, media, subscribe-proxy +│ │ │ ├── plugins/ # db, redis, centrifugo client, auth/jwt +│ │ │ ├── db/ # migrations + queries +│ │ │ └── server.ts +│ │ └── package.json +│ ├── web/ # React + Vite; imports core +│ │ └── package.json +│ ├── mobile/ # React Native + Expo; imports core +│ │ └── package.json +│ └── desktop/ # Tauri/Electron shell over web build (config-heavy) +│ └── package.json +├── infra/ # docker-compose for Centrifugo, Postgres, Redis, MinIO +│ ├── docker-compose.yml +│ └── centrifugo/config.json +└── PROJECT_SPEC.md +``` + +### Rules for `core` + +- No `react-dom` or `react-native` imports anywhere in `core`. It is pure TS. +- Keep React hooks / state logic UI-agnostic where possible so it can migrate into `core` + later. Do NOT bake DOM/RN assumptions into the Centrifugo/API wrappers. +- One source of truth for payload shapes: types + JSON Schemas live here, backend and + clients both consume them. + +## 5. Data model (starting point — refine in planning) + +Tables (Postgres): `users`, `rooms`, `room_members`, `conversations` (for DMs), +`messages`, `read_state` (per-user, per-conversation last-read message id). + +- `messages` needs a **server-assigned monotonic id/sequence** (e.g. BIGSERIAL or per-room + counter) and a stable id used for client-side dedupe. +- Membership drives channel access (the subscribe-proxy checks these tables). + +## 6. Build phases (implement in this order) + +**Phase 0 — Skeleton.** Monorepo tooling, workspaces, tsconfig, empty packages that build +and import `core`. `infra/docker-compose.yml` bringing up Centrifugo + Postgres + Redis + +MinIO locally. No features yet. Verify everything boots. + +**Phase 1 — Auth + connection.** User signup/login in backend; JWT minting for Centrifugo; +web client connects its (receive-only) socket and subscribes to its personal channel via +the subscribe-proxy. Prove end-to-end connectivity. + +**Phase 2 — Rooms + messaging.** Room CRUD, membership, subscribe-proxy authorization, +send-message REST endpoint → persist → publish to `room:`. History load from Postgres. +Server-assigned ids + client dedupe. Web client sends/receives in a room. + +**Phase 3 — DMs.** Personal-channel delivery (publish to both participants), conversation +model, client routing by `conversationId`. + +**Phase 4 — Media messages.** MinIO presigned uploads; voice notes / video messages / +images as references published through channels. + +**Phase 5 — Mobile client.** React Native + Expo consuming the same `core`. Reuse realtime + +- API layer; platform-specific UI only. + +**Phase 6 — Desktop shell.** Tauri/Electron over the web build. + +**Phase 7 (LATER, separate effort) — Calls.** LiveKit + coturn, call signaling over +Centrifugo (ring via personal channel), token minting in backend, screen share. Do NOT +start until explicitly told; video across platforms is the hardest part of the project and +gets its own focused phase. + +## 7. Scaling assumptions (design for, don't build yet) + +- Backend and Centrifugo are **stateless** — no in-memory session state; everything in + Postgres/Redis. This is what lets them scale horizontally later. +- Media plane (LiveKit/coturn) stays **physically separate** from the control plane. +- Redis is present from Phase 0 (coordination layer for later Centrifugo + LiveKit clustering). + +## 8. How to work (IMPORTANT — follow this) + +- **Plan first.** Before writing code, produce a plan for the current phase only and wait + for my approval. Do not jump ahead to later phases. +- **Explain before implementing.** For each phase, briefly explain the approach and the + reasoning behind key decisions before generating code. I prefer understanding the "why". +- **Go part by part with check-ins.** Implement one coherent piece, stop, let me review, + then continue. Do not dump large amounts of generated code in one go. +- **Do not add features I didn't ask for.** No calls/WebRTC before Phase 7. No speculative + abstractions. +- **Ask when a decision is ambiguous** rather than guessing, especially on the data model + and API shapes. +- **Respect the decided stack** in section 3 — don't substitute frameworks or add heavy + dependencies without flagging why. +- Keep the shared `core` package clean of platform-specific imports. + +## 9. Legal / deployment note (context, not a task) + +Deployment target includes the Russian Federation. There are messenger-registration and +data-localization rules that carry real liability; this is for the humans to handle with +local legal advice — not something to implement or work around in code. Network reliability +on poor connections (TURN-over-443, simulcast) is legitimate engineering and belongs in the +LATER calls phase, but is not a censorship-circumvention feature and should not be built as one. diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..9918e08 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,35 @@ +# Local-dev conveniences — auto-loaded by `docker compose up`. Publishes service +# ports to the host and runs the backend with hot reload (tsx watch). Not for prod. +services: + postgres: + ports: + - '${POSTGRES_PORT:-5432}:5432' + + redis: + ports: + - '${REDIS_PORT:-6379}:6379' + + minio: + ports: + - '${MINIO_PORT:-9000}:9000' + - '9001:9001' + + centrifugo: + ports: + - '8000:8000' + + backend: + build: + context: . + dockerfile: packages/backend/Dockerfile + target: dev + command: ['pnpm', 'dev'] + volumes: + - ./packages/backend/src:/repo/packages/backend/src + - ./packages/core/src:/repo/packages/core/src + ports: + - '${BACKEND_PORT:-4000}:4000' + + nginx: + ports: + - '${NGINX_HTTP_PORT:-8080}:80' diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..bd53da0 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,24 @@ +# Deploy overrides — use explicitly: +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d +# No source mounts, restart policies, and only nginx faces the host. +services: + postgres: + restart: unless-stopped + + redis: + restart: unless-stopped + + minio: + restart: unless-stopped + + centrifugo: + restart: unless-stopped + + backend: + restart: unless-stopped + + nginx: + restart: unless-stopped + ports: + - '80:80' + - '443:443' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2068407 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,130 @@ +# Base stack — all server-side services. Self-contained: `docker compose up` +# builds and boots everything. No host ports are published here (only the dev +# override and prod files expose ports); services talk over the private network. +name: altricade + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + - ./infra/postgres/init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] + interval: 10s + timeout: 5s + retries: 5 + networks: [altricade] + + redis: + image: redis:7-alpine + command: ['redis-server', '--appendonly', 'yes'] + volumes: + - redis-data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + networks: [altricade] + + minio: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + command: ['server', '/data', '--console-address', ':9001'] + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + volumes: + - minio-data:/data + healthcheck: + test: ['CMD-SHELL', 'curl -sf http://localhost:9000/minio/health/live || exit 1'] + interval: 10s + timeout: 5s + retries: 5 + networks: [altricade] + + # One-shot: create the media + avatars buckets, then exit. + minio-setup: + image: minio/mc:RELEASE.2025-04-16T18-13-26Z + depends_on: + minio: + condition: service_healthy + env_file: .env + entrypoint: + - /bin/sh + - -c + - | + mc alias set local http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}" && + mc mb --ignore-existing "local/$${MINIO_BUCKET_MEDIA}" && + mc mb --ignore-existing "local/$${MINIO_BUCKET_AVATARS}" && + echo "minio buckets ready" + restart: 'no' + networks: [altricade] + + centrifugo: + image: centrifugo/centrifugo:v6 + command: ['centrifugo', '-c', '/centrifugo/config.json'] + environment: + CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY: ${CENTRIFUGO_TOKEN_HMAC_SECRET} + CENTRIFUGO_HTTP_API_KEY: ${CENTRIFUGO_API_KEY} + volumes: + - ./infra/centrifugo/config.json:/centrifugo/config.json:ro + depends_on: + redis: + condition: service_healthy + ulimits: + nofile: + soft: 65536 + hard: 65536 + networks: [altricade] + + # One-shot: run DB migrations, then exit. Backend waits for this to complete. + migrate: + build: + context: . + dockerfile: packages/backend/Dockerfile + command: ['node_modules/.bin/node-pg-migrate', 'up'] + env_file: .env + depends_on: + postgres: + condition: service_healthy + restart: 'no' + networks: [altricade] + + backend: + build: + context: . + dockerfile: packages/backend/Dockerfile + env_file: .env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + migrate: + condition: service_completed_successfully + networks: [altricade] + + nginx: + image: nginx:1.27-alpine + volumes: + - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - backend + - centrifugo + networks: [altricade] + +volumes: + postgres-data: + redis-data: + minio-data: + +networks: + altricade: + driver: bridge diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..1462212 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,86 @@ +// Shared flat ESLint config — the mechanical enforcement of the project's +// non-negotiable code-quality standards. Each package's eslint.config.mjs +// imports `base` from here and appends its own architecture-boundary rules. +// +// Enforced here (build fails on violation): +// - no `any` (@typescript-eslint/no-explicit-any + no-unsafe-*) +// - no type assertions (consistent-type-assertions: never → bans `x as T`) +// - no non-null `!` (no-non-null-assertion) +// - no dead optional `?.`(no-unnecessary-condition) +// - functional iteration (prefer-const, no-param-reassign, no-var) +// - no suppressions (ban-ts-comment) +// - safe promises (no-floating-promises, await-thenable) + +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export const base = tseslint.config( + { + ignores: ['**/dist/**', '**/build/**', '**/.expo/**', '**/coverage/**', '**/*.d.ts'], + }, + js.configs.recommended, + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + globals: { + ...globals.node, + }, + }, + rules: { + // --- no any --- + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + + // --- no type assertions (bans `x as T`; `as const` still allowed) --- + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { assertionStyle: 'never' }, + ], + + // --- no non-null assertions --- + '@typescript-eslint/no-non-null-assertion': 'error', + + // --- no error-silencing optional chaining / dead conditions --- + '@typescript-eslint/no-unnecessary-condition': 'error', + + // --- functional / immutable iteration; no spaghetti --- + 'prefer-const': 'error', + 'no-param-reassign': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + + // --- no suppressions --- + '@typescript-eslint/ban-ts-comment': 'error', + + // --- safe async --- + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + + // --- hygiene --- + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + '@typescript-eslint/consistent-type-imports': 'error', + }, + }, + { + // Config files themselves run outside the typed project graph. + files: ['**/*.config.{js,mjs,cjs,ts}', '**/*.setup.{js,mjs,cjs,ts}'], + ...tseslint.configs.disableTypeChecked, + }, + prettier, +); + +export default base; diff --git a/infra/centrifugo/config.json b/infra/centrifugo/config.json new file mode 100644 index 0000000..05f15ac --- /dev/null +++ b/infra/centrifugo/config.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/centrifugal/centrifugo/master/internal/config/schema.json", + "client": { + "allowed_origins": ["*"] + }, + "http_api": { + "enabled": true + }, + "engine": { + "type": "redis", + "redis": { + "address": "redis://redis:6379" + } + }, + "channel": { + "namespaces": [ + { + "name": "room", + "presence": true, + "join_leave": true, + "force_push_join_leave": true, + "history_size": 100, + "history_ttl": "300s", + "force_recovery": true, + "subscribe_proxy_enabled": true, + "subscribe_proxy_name": "backend" + }, + { + "name": "user", + "presence": true, + "history_size": 100, + "history_ttl": "300s", + "force_recovery": true, + "subscribe_proxy_enabled": true, + "subscribe_proxy_name": "backend" + } + ] + }, + "proxies": [ + { + "name": "backend", + "endpoint": "http://backend:4000/centrifugo/subscribe", + "timeout": "3s" + } + ] +} diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf new file mode 100644 index 0000000..77ef535 --- /dev/null +++ b/infra/nginx/nginx.conf @@ -0,0 +1,45 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + # Upstreams resolve to compose service names on the private network. + upstream backend { + server backend:4000; + } + upstream centrifugo { + server centrifugo:8000; + } + + server { + listen 80; + server_name _; + + # REST API — strip the /api prefix before proxying to the backend. + location /api/ { + proxy_pass http://backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Realtime WebSocket — Centrifugo (receive-only client socket). + location /connection/websocket { + proxy_pass http://centrifugo; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 3600s; + } + + location / { + default_type text/plain; + return 200 "Altricade gateway — /api/* -> backend, /connection/websocket -> centrifugo\n"; + } + } +} diff --git a/infra/postgres/init/.gitkeep b/infra/postgres/init/.gitkeep new file mode 100644 index 0000000..ce4e7b3 --- /dev/null +++ b/infra/postgres/init/.gitkeep @@ -0,0 +1,2 @@ +# Optional Postgres init SQL is mounted here (docker-entrypoint-initdb.d). +# Schema is managed by node-pg-migrate migrations, not init scripts. diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f784b4 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "altricade-messenger", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@9.12.3", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev", + "lint": "turbo run lint", + "typecheck": "turbo run typecheck", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.6.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-boundaries": "^7.0.2", + "globals": "^17.7.0", + "prettier": "^3.9.5", + "turbo": "^2.10.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.63.0" + }, + "pnpm": { + "overrides": { + "esbuild": ">=0.28.1" + } + } +} diff --git a/packages/backend/.dockerignore b/packages/backend/.dockerignore new file mode 100644 index 0000000..cf4cb51 --- /dev/null +++ b/packages/backend/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.turbo +*.tsbuildinfo +.env +.env.* diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile new file mode 100644 index 0000000..2561098 --- /dev/null +++ b/packages/backend/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1 + +# ---- base: pnpm-enabled Node ---- +FROM node:22-alpine AS base +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable +WORKDIR /repo + +# ---- build: install workspace, bundle backend (+ core) ---- +FROM base AS build +# Manifests first for better layer caching. +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json tsconfig.base.json ./ +COPY packages/core/package.json packages/core/package.json +COPY packages/backend/package.json packages/backend/package.json +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile +# Sources. +COPY packages/core packages/core +COPY packages/backend packages/backend +# Bundle backend + core source into packages/backend/dist. +RUN pnpm --filter @altricade/backend build +# Produce a pruned, production-only deployment (dist + migrations + prod node_modules). +RUN pnpm --filter @altricade/backend deploy --prod /app + +# ---- dev: hot-reload for local development (used by docker-compose.override.yml) ---- +# Inherits the full workspace + dev deps (tsx) from the build stage. Source is +# bind-mounted over this at runtime so edits reload live. +FROM build AS dev +WORKDIR /repo/packages/backend +EXPOSE 4000 +CMD ["pnpm", "dev"] + +# ---- runtime: slim, non-root ---- +FROM base AS runtime +ENV NODE_ENV=production +WORKDIR /app +COPY --from=build --chown=node:node /app /app +USER node +EXPOSE 4000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget -qO- "http://127.0.0.1:${BACKEND_PORT:-4000}/health" || exit 1 +CMD ["node", "dist/server.js"] diff --git a/packages/backend/eslint.config.mjs b/packages/backend/eslint.config.mjs new file mode 100644 index 0000000..eaab6fa --- /dev/null +++ b/packages/backend/eslint.config.mjs @@ -0,0 +1,45 @@ +import { base } from '../../eslint.config.mjs'; +import boundaries from 'eslint-plugin-boundaries'; + +// Backend architecture layering. The folder-based layers are classified and the +// import direction between them is enforced: +// routes → may import plugins, db +// plugins → may import db +// db → leaf (types only) +// (src/app.ts, src/server.ts, src/config.ts are the composition root + leaf +// config — unclassified in Phase 0.) The full per-domain module structure with +// route → service → repository layering is introduced in Phase 1. +export default [ + { ignores: ['dist/**', 'migrations/**'] }, + ...base, + { + files: ['src/**/*.ts'], + plugins: { boundaries }, + settings: { + 'boundaries/include': ['src/**/*'], + 'boundaries/elements': [ + { type: 'plugins', pattern: 'src/plugins/*' }, + { type: 'routes', pattern: 'src/routes/*' }, + { type: 'db', pattern: 'src/db/*' }, + ], + }, + rules: { + 'boundaries/dependencies': [ + 'error', + { + default: 'disallow', + policies: [ + { + from: { element: { types: 'routes' } }, + allow: { to: { element: { types: { anyOf: ['plugins', 'db'] } } } }, + }, + { + from: { element: { types: 'plugins' } }, + allow: { to: { element: { types: 'db' } } }, + }, + ], + }, + ], + }, + }, +]; diff --git a/packages/backend/migrations/1720000000000_init.cjs b/packages/backend/migrations/1720000000000_init.cjs new file mode 100644 index 0000000..741cc08 --- /dev/null +++ b/packages/backend/migrations/1720000000000_init.cjs @@ -0,0 +1,13 @@ +// Phase 0 placeholder migration — proves the node-pg-migrate pipeline runs +// end-to-end against Postgres. The real schema (users, refresh_tokens, rooms, +// room_members, conversations, messages, reactions, read_state) lands in Phase 1. + +/** @param {import('node-pg-migrate').MigrationBuilder} pgm */ +exports.up = (pgm) => { + pgm.createExtension('pgcrypto', { ifNotExists: true }); +}; + +/** @param {import('node-pg-migrate').MigrationBuilder} pgm */ +exports.down = (pgm) => { + pgm.dropExtension('pgcrypto', { ifExists: true }); +}; diff --git a/packages/backend/package.json b/packages/backend/package.json new file mode 100644 index 0000000..296c6f4 --- /dev/null +++ b/packages/backend/package.json @@ -0,0 +1,33 @@ +{ + "name": "@altricade/backend", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsup", + "start": "node dist/server.js", + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "migrate:up": "node-pg-migrate up", + "migrate:down": "node-pg-migrate down" + }, + "dependencies": { + "@altricade/core": "workspace:^", + "@fastify/cors": "^11.3.0", + "fastify": "^5.10.0", + "fastify-plugin": "^6.0.0", + "ioredis": "^5.11.1", + "kysely": "^0.29.3", + "minio": "^8.0.7", + "node-pg-migrate": "^8.0.4", + "pg": "^8.22.0" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "@types/pg": "^8.20.0", + "tsup": "^8.5.1", + "tsx": "^4.23.0", + "typescript": "^5.9.3" + } +} diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts new file mode 100644 index 0000000..b81b872 --- /dev/null +++ b/packages/backend/src/app.ts @@ -0,0 +1,39 @@ +import Fastify from 'fastify'; +import type { FastifyInstance } from 'fastify'; +import cors from '@fastify/cors'; +import { roomChannel } from '@altricade/core'; +import { loadConfig } from './config'; +import type { AppConfig } from './config'; +import { dbPlugin } from './plugins/db'; +import { redisPlugin } from './plugins/redis'; +import { minioPlugin } from './plugins/minio'; +import { centrifugoPlugin } from './plugins/centrifugo'; +import { healthRoutes } from './routes/health'; + +declare module 'fastify' { + interface FastifyInstance { + config: AppConfig; + } +} + +// Composition root: assemble the Fastify app from config + plugins + routes. +// Kept side-effect-free (no listen) so it can be reused by tests later. +export const buildApp = async (config: AppConfig = loadConfig()): Promise => { + const app = Fastify({ + logger: { level: config.nodeEnv === 'production' ? 'info' : 'debug' }, + }); + + app.decorate('config', config); + + await app.register(cors, { origin: true, credentials: true }); + await app.register(dbPlugin); + await app.register(redisPlugin); + await app.register(minioPlugin); + await app.register(centrifugoPlugin); + await app.register(healthRoutes); + + // Proves the @altricade/core shared kernel is imported and callable. + app.log.info(`core wired — example channel: ${roomChannel('demo')}`); + + return app; +}; diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts new file mode 100644 index 0000000..c8b7b60 --- /dev/null +++ b/packages/backend/src/config.ts @@ -0,0 +1,79 @@ +// Typed, validated environment configuration. All backend config comes from env +// (12-factor); missing or malformed required values fail fast at startup rather +// than surfacing as confusing runtime errors. No `any`, no assertions. + +export interface MinioConfig { + endpoint: string; + port: number; + useSSL: boolean; + accessKey: string; + secretKey: string; + buckets: { + media: string; + avatars: string; + }; +} + +export interface CentrifugoConfig { + apiUrl: string; + apiKey: string; + tokenHmacSecret: string; +} + +export interface AppConfig { + nodeEnv: string; + port: number; + databaseUrl: string; + redisUrl: string; + minio: MinioConfig; + centrifugo: CentrifugoConfig; +} + +const required = (name: string): string => { + const value = process.env[name]; + if (value === undefined || value === '') { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +}; + +const optional = (name: string, fallback: string): string => { + const value = process.env[name]; + if (value === undefined || value === '') { + return fallback; + } + return value; +}; + +const parsePort = (name: string, raw: string): number => { + const port = Number.parseInt(raw, 10); + if (Number.isNaN(port) || port <= 0 || port > 65535) { + throw new Error(`Environment variable ${name} is not a valid port: ${raw}`); + } + return port; +}; + +const parseBoolean = (raw: string): boolean => raw.toLowerCase() === 'true'; + +export const loadConfig = (): AppConfig => ({ + nodeEnv: optional('NODE_ENV', 'development'), + port: parsePort('BACKEND_PORT', optional('BACKEND_PORT', '4000')), + databaseUrl: required('DATABASE_URL'), + redisUrl: required('REDIS_URL'), + minio: { + endpoint: required('MINIO_ENDPOINT'), + port: parsePort('MINIO_PORT', optional('MINIO_PORT', '9000')), + useSSL: parseBoolean(optional('MINIO_USE_SSL', 'false')), + accessKey: required('MINIO_ROOT_USER'), + secretKey: required('MINIO_ROOT_PASSWORD'), + buckets: { + media: optional('MINIO_BUCKET_MEDIA', 'media'), + avatars: optional('MINIO_BUCKET_AVATARS', 'avatars'), + }, + }, + centrifugo: { + apiUrl: required('CENTRIFUGO_API_URL'), + apiKey: required('CENTRIFUGO_API_KEY'), + tokenHmacSecret: required('CENTRIFUGO_TOKEN_HMAC_SECRET'), + }, +}); diff --git a/packages/backend/src/db/schema.ts b/packages/backend/src/db/schema.ts new file mode 100644 index 0000000..6649188 --- /dev/null +++ b/packages/backend/src/db/schema.ts @@ -0,0 +1,4 @@ +// The Kysely database registry: one property per table. Empty in Phase 0 — +// tables (users, refresh_tokens, rooms, messages, ...) are added here in lockstep +// with node-pg-migrate migrations from Phase 1 onward. +export type Database = Record; diff --git a/packages/backend/src/plugins/centrifugo.ts b/packages/backend/src/plugins/centrifugo.ts new file mode 100644 index 0000000..729d3c3 --- /dev/null +++ b/packages/backend/src/plugins/centrifugo.ts @@ -0,0 +1,41 @@ +import fp from 'fastify-plugin'; + +export interface CentrifugoClient { + /** Publish an event payload into a channel via Centrifugo's server HTTP API. */ + publish(channel: string, data: unknown): Promise; +} + +declare module 'fastify' { + interface FastifyInstance { + centrifugo: CentrifugoClient; + } +} + +// Thin wrapper over Centrifugo's server HTTP API. Centrifugo is a dumb pipe: the +// backend is the only publisher. Wired now; actually used from Phase 2 onward. +export const centrifugoPlugin = fp( + (app) => { + const { apiUrl, apiKey } = app.config.centrifugo; + + const client: CentrifugoClient = { + async publish(channel, data) { + const response = await fetch(`${apiUrl}/publish`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ channel, data }), + }); + if (!response.ok) { + throw new Error(`Centrifugo publish failed with status ${String(response.status)}`); + } + }, + }; + + app.decorate('centrifugo', client); + + return Promise.resolve(); + }, + { name: 'centrifugo' }, +); diff --git a/packages/backend/src/plugins/db.ts b/packages/backend/src/plugins/db.ts new file mode 100644 index 0000000..9ced507 --- /dev/null +++ b/packages/backend/src/plugins/db.ts @@ -0,0 +1,27 @@ +import fp from 'fastify-plugin'; +import pg from 'pg'; +import { Kysely, PostgresDialect } from 'kysely'; +import type { Database } from '../db/schema'; + +declare module 'fastify' { + interface FastifyInstance { + db: Kysely; + } +} + +// Postgres connection pool wrapped in a typed Kysely instance. Registered via +// fastify-plugin so the `db` decoration is visible app-wide, not just in scope. +export const dbPlugin = fp( + (app) => { + const pool = new pg.Pool({ connectionString: app.config.databaseUrl }); + const db = new Kysely({ dialect: new PostgresDialect({ pool }) }); + + app.decorate('db', db); + app.addHook('onClose', async () => { + await db.destroy(); + }); + + return Promise.resolve(); + }, + { name: 'db' }, +); diff --git a/packages/backend/src/plugins/minio.ts b/packages/backend/src/plugins/minio.ts new file mode 100644 index 0000000..62f998f --- /dev/null +++ b/packages/backend/src/plugins/minio.ts @@ -0,0 +1,29 @@ +import fp from 'fastify-plugin'; +import { Client } from 'minio'; + +declare module 'fastify' { + interface FastifyInstance { + minio: Client; + } +} + +// S3-compatible object storage client (media messages + avatars). Buckets are +// bootstrapped by the compose `minio-setup` one-shot; this client is used for +// readiness checks now and presigned uploads from Phase 6. +export const minioPlugin = fp( + (app) => { + const { endpoint, port, useSSL, accessKey, secretKey } = app.config.minio; + const client = new Client({ + endPoint: endpoint, + port, + useSSL, + accessKey, + secretKey, + }); + + app.decorate('minio', client); + + return Promise.resolve(); + }, + { name: 'minio' }, +); diff --git a/packages/backend/src/plugins/redis.ts b/packages/backend/src/plugins/redis.ts new file mode 100644 index 0000000..5b0e9fc --- /dev/null +++ b/packages/backend/src/plugins/redis.ts @@ -0,0 +1,27 @@ +import fp from 'fastify-plugin'; +import { Redis } from 'ioredis'; + +declare module 'fastify' { + interface FastifyInstance { + redis: Redis; + } +} + +// Redis client. Present from Phase 0 as the coordination layer Centrifugo (and +// later LiveKit) clustering depends on; also used for auth rate-limiting later. +export const redisPlugin = fp( + (app) => { + const redis = new Redis(app.config.redisUrl, { + maxRetriesPerRequest: null, + lazyConnect: false, + }); + + app.decorate('redis', redis); + app.addHook('onClose', async () => { + await redis.quit(); + }); + + return Promise.resolve(); + }, + { name: 'redis' }, +); diff --git a/packages/backend/src/routes/health.ts b/packages/backend/src/routes/health.ts new file mode 100644 index 0000000..dbd1b8b --- /dev/null +++ b/packages/backend/src/routes/health.ts @@ -0,0 +1,86 @@ +import { sql } from 'kysely'; +import type { FastifyInstance } from 'fastify'; + +interface ReadyReport { + status: 'ready' | 'degraded'; + checks: { + postgres: boolean; + redis: boolean; + minio: boolean; + }; +} + +// Bound each dependency probe so /ready always answers promptly, even when a +// backing service hangs (e.g. a disconnected client queuing commands rather than +// failing fast). A probe that does not resolve in time counts as not-ready. +const PROBE_TIMEOUT_MS = 2000; + +const withTimeout = async (probe: Promise): Promise => { + let timer: ReturnType | undefined; + const timeout = new Promise((resolve) => { + timer = setTimeout(() => { + resolve(false); + }, PROBE_TIMEOUT_MS); + }); + try { + return await Promise.race([probe, timeout]); + } finally { + if (timer !== undefined) { + clearTimeout(timer); + } + } +}; + +const checkPostgres = async (app: FastifyInstance): Promise => { + try { + await sql`select 1`.execute(app.db); + return true; + } catch (error) { + app.log.warn({ error }, 'postgres readiness check failed'); + return false; + } +}; + +const checkRedis = async (app: FastifyInstance): Promise => { + try { + await app.redis.ping(); + return true; + } catch (error) { + app.log.warn({ error }, 'redis readiness check failed'); + return false; + } +}; + +const checkMinio = async (app: FastifyInstance): Promise => { + try { + await app.minio.listBuckets(); + return true; + } catch (error) { + app.log.warn({ error }, 'minio readiness check failed'); + return false; + } +}; + +// /health — liveness: the process is up and serving (used by Docker HEALTHCHECK). +// /ready — readiness: every backing service is reachable (pg + redis + minio). +export const healthRoutes = (app: FastifyInstance): Promise => { + app.get('/health', () => ({ status: 'ok' })); + + app.get('/ready', async (_request, reply) => { + const [postgres, redis, minio] = await Promise.all([ + withTimeout(checkPostgres(app)), + withTimeout(checkRedis(app)), + withTimeout(checkMinio(app)), + ]); + + const ready = postgres && redis && minio; + const report: ReadyReport = { + status: ready ? 'ready' : 'degraded', + checks: { postgres, redis, minio }, + }; + + return reply.code(ready ? 200 : 503).send(report); + }); + + return Promise.resolve(); +}; diff --git a/packages/backend/src/server.ts b/packages/backend/src/server.ts new file mode 100644 index 0000000..59ed172 --- /dev/null +++ b/packages/backend/src/server.ts @@ -0,0 +1,29 @@ +import { buildApp } from './app'; +import { loadConfig } from './config'; + +const start = async (): Promise => { + const config = loadConfig(); + const app = await buildApp(config); + + const shutdown = async (signal: string): Promise => { + app.log.info(`received ${signal}, shutting down gracefully`); + await app.close(); + process.exit(0); + }; + + process.on('SIGTERM', () => { + void shutdown('SIGTERM'); + }); + process.on('SIGINT', () => { + void shutdown('SIGINT'); + }); + + try { + await app.listen({ host: '0.0.0.0', port: config.port }); + } catch (error) { + app.log.error(error); + process.exit(1); + } +}; + +void start(); diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json new file mode 100644 index 0000000..d3081eb --- /dev/null +++ b/packages/backend/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/backend/tsup.config.ts b/packages/backend/tsup.config.ts new file mode 100644 index 0000000..2054111 --- /dev/null +++ b/packages/backend/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +// Bundle the backend (and the @altricade/core source it imports) into a single +// self-contained ESM output for the slim runtime image. Third-party deps stay +// external and are installed as production deps in the Docker runtime stage. +export default defineConfig({ + entry: ['src/server.ts'], + format: ['esm'], + target: 'node22', + platform: 'node', + outDir: 'dist', + clean: true, + sourcemap: true, + noExternal: [/^@altricade\//], +}); diff --git a/packages/core/eslint.config.mjs b/packages/core/eslint.config.mjs new file mode 100644 index 0000000..56d4960 --- /dev/null +++ b/packages/core/eslint.config.mjs @@ -0,0 +1,3 @@ +import { base } from '../../eslint.config.mjs'; + +export default base; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..87d7a1a --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,17 @@ +{ + "name": "@altricade/core", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./channels": "./src/channels/index.ts", + "./events": "./src/events/index.ts", + "./types": "./src/types/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit", + "lint": "eslint ." + } +} diff --git a/packages/core/src/channels/index.ts b/packages/core/src/channels/index.ts new file mode 100644 index 0000000..5b6c594 --- /dev/null +++ b/packages/core/src/channels/index.ts @@ -0,0 +1,21 @@ +// Channel-name builders — the single source of truth for Centrifugo channel +// names, shared by backend (publish/subscribe-proxy) and every client. +// +// room: — one channel per room; all members subscribe. +// user: — one personal channel per user; DM + notification delivery. + +export type RoomId = string; +export type UserId = string; + +const ROOM_PREFIX = 'room:'; +const USER_PREFIX = 'user:'; + +export const roomChannel = (roomId: RoomId): string => `${ROOM_PREFIX}${roomId}`; + +export const userChannel = (userId: UserId): string => `${USER_PREFIX}${userId}`; + +/** True for a `room:` channel name. */ +export const isRoomChannel = (channel: string): boolean => channel.startsWith(ROOM_PREFIX); + +/** True for a `user:` channel name. */ +export const isUserChannel = (channel: string): boolean => channel.startsWith(USER_PREFIX); diff --git a/packages/core/src/events/index.ts b/packages/core/src/events/index.ts new file mode 100644 index 0000000..9fc0e8f --- /dev/null +++ b/packages/core/src/events/index.ts @@ -0,0 +1,31 @@ +// Realtime event taxonomy (spec section 3). Every live feature is an event on +// the same receive socket; these names are the contract between backend +// publishers and client handlers. Payload shapes are added alongside features. +// +// Bucket A — durable, must be correct → through the backend (persist, publish) +// Bucket B — ephemeral, throwaway → published, not persisted +// Bucket C — connection-derived → Centrifugo built-in presence + +export const EventType = { + // Bucket A + MessageNew: 'message.new', + MessageEdit: 'message.edit', + MessageDelete: 'message.delete', + ReactionAdd: 'reaction.add', + ReactionRemove: 'reaction.remove', + ReadReceipt: 'read.receipt', + LastSeen: 'last_seen', + RoomMembership: 'room.membership', + ProfileUpdate: 'profile.update', + + // Bucket B + TypingStart: 'typing.start', + TypingStop: 'typing.stop', + RecordingAudio: 'recording.audio', + RecordingVideo: 'recording.video', + + // Bucket C + Presence: 'presence', +} as const; + +export type EventType = (typeof EventType)[keyof typeof EventType]; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..6675655 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,4 @@ +// Public API of the @altricade/core shared kernel. +export * from './channels/index'; +export * from './events/index'; +export * from './types/index'; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts new file mode 100644 index 0000000..dc975e0 --- /dev/null +++ b/packages/core/src/types/index.ts @@ -0,0 +1,20 @@ +// Shared payload/domain types. Placeholder shapes for Phase 0 — real message / +// user / API types (and their JSON Schemas) are filled in per feature phase. + +/** Bumped as `core` evolves; clients can log/verify the shared contract version. */ +export const CORE_VERSION = '0.0.0'; + +export interface User { + id: string; + username: string; + displayName: string; + avatarUrl: string | null; +} + +export interface Message { + id: string; + conversationId: string; + senderId: string; + body: string; + createdAt: string; +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..da8829f --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/desktop/eslint.config.mjs b/packages/desktop/eslint.config.mjs new file mode 100644 index 0000000..56d4960 --- /dev/null +++ b/packages/desktop/eslint.config.mjs @@ -0,0 +1,3 @@ +import { base } from '../../eslint.config.mjs'; + +export default base; diff --git a/packages/desktop/package.json b/packages/desktop/package.json new file mode 100644 index 0000000..992b0f0 --- /dev/null +++ b/packages/desktop/package.json @@ -0,0 +1,13 @@ +{ + "name": "@altricade/desktop", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@altricade/core": "workspace:^" + } +} diff --git a/packages/desktop/src/index.ts b/packages/desktop/src/index.ts new file mode 100644 index 0000000..69173ac --- /dev/null +++ b/packages/desktop/src/index.ts @@ -0,0 +1,9 @@ +// Phase 0 placeholder. The desktop client is a thin Tauri shell wrapping the +// @altricade/web build (macOS + Windows), built in Phase 7. No separate UI — +// it loads the same web app, so it inherits web's FSD structure, theming, and +// offline-first behavior. + +import { CORE_VERSION } from '@altricade/core'; + +export const desktopPlaceholder = (): string => + `@altricade/desktop (Tauri shell) stub on core ${CORE_VERSION}`; diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 0000000..da8829f --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/mobile/eslint.config.mjs b/packages/mobile/eslint.config.mjs new file mode 100644 index 0000000..56d4960 --- /dev/null +++ b/packages/mobile/eslint.config.mjs @@ -0,0 +1,3 @@ +import { base } from '../../eslint.config.mjs'; + +export default base; diff --git a/packages/mobile/package.json b/packages/mobile/package.json new file mode 100644 index 0000000..b562224 --- /dev/null +++ b/packages/mobile/package.json @@ -0,0 +1,13 @@ +{ + "name": "@altricade/mobile", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@altricade/core": "workspace:^" + } +} diff --git a/packages/mobile/src/index.ts b/packages/mobile/src/index.ts new file mode 100644 index 0000000..2c584cf --- /dev/null +++ b/packages/mobile/src/index.ts @@ -0,0 +1,14 @@ +// Phase 0 placeholder. The real React Native + Expo app is built in Phase 7, +// reusing the @altricade/core realtime/API/auth layer with platform-specific UI +// following the same Feature-Sliced Design layers as web +// (app → screens → widgets → features → entities → shared). +// +// Decisions locked for Phase 7: +// - Virtualized lists use @legendapp/list (LegendList), NOT FlatList. +// - Offline-first: persistent outbox + local cache, shared logic from core. +// - Light/dark theming from shared design tokens (mirrors web/src/shared/theme). + +import { CORE_VERSION, userChannel } from '@altricade/core'; + +export const mobilePlaceholder = (): string => + `@altricade/mobile stub on core ${CORE_VERSION}; personal channel ${userChannel('me')}`; diff --git a/packages/mobile/tsconfig.json b/packages/mobile/tsconfig.json new file mode 100644 index 0000000..da8829f --- /dev/null +++ b/packages/mobile/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/web/eslint.config.mjs b/packages/web/eslint.config.mjs new file mode 100644 index 0000000..75b21b0 --- /dev/null +++ b/packages/web/eslint.config.mjs @@ -0,0 +1,44 @@ +import { base } from '../../eslint.config.mjs'; +import boundaries from 'eslint-plugin-boundaries'; +import reactHooks from 'eslint-plugin-react-hooks'; +import globals from 'globals'; + +// Strict Feature-Sliced Design: a layer may import only from itself and the +// layers below it. Upward imports (e.g. shared → features) are build errors. +const FSD_LAYERS = ['app', 'pages', 'widgets', 'features', 'entities', 'shared']; + +const fsdPolicies = FSD_LAYERS.map((layer, index) => ({ + from: { element: { types: layer } }, + allow: { to: { element: { types: { anyOf: FSD_LAYERS.slice(index) } } } }, +})); + +export default [ + { ignores: ['dist/**'] }, + ...base, + { + files: ['src/**/*.{ts,tsx}'], + languageOptions: { + globals: { ...globals.browser }, + }, + plugins: { + boundaries, + 'react-hooks': reactHooks, + }, + settings: { + 'boundaries/include': ['src/**/*'], + 'boundaries/elements': [ + { type: 'app', pattern: 'src/app/*' }, + { type: 'pages', pattern: 'src/pages/*' }, + { type: 'widgets', pattern: 'src/widgets/*' }, + { type: 'features', pattern: 'src/features/*' }, + { type: 'entities', pattern: 'src/entities/*' }, + { type: 'shared', pattern: 'src/shared/*' }, + ], + }, + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'boundaries/dependencies': ['error', { default: 'disallow', policies: fsdPolicies }], + }, + }, +]; diff --git a/packages/web/index.html b/packages/web/index.html new file mode 100644 index 0000000..29e09c7 --- /dev/null +++ b/packages/web/index.html @@ -0,0 +1,12 @@ + + + + + + Altricade + + +
+ + + diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000..5c5b255 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "@altricade/web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@altricade/core": "workspace:^", + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "eslint-plugin-react-hooks": "^7.1.1", + "typescript": "^5.9.3", + "vite": "^8.1.4" + } +} diff --git a/packages/web/src/app/App.tsx b/packages/web/src/app/App.tsx new file mode 100644 index 0000000..bf224d2 --- /dev/null +++ b/packages/web/src/app/App.tsx @@ -0,0 +1,44 @@ +import type { ReactElement } from 'react'; +import { CORE_VERSION, roomChannel } from '@altricade/core'; +import { useTheme } from '../shared/theme'; +import type { ThemePreference } from '../shared/theme'; +import { env } from '../shared/config'; + +const PREFERENCES: readonly ThemePreference[] = ['light', 'dark', 'system']; + +export const App = (): ReactElement => { + const { preference, resolved, setPreference } = useTheme(); + + return ( +
+

Altricade

+

Realtime chat & calls — Phase 0 skeleton.

+
+
core version
+
{CORE_VERSION}
+
example channel
+
{roomChannel('demo')}
+
API base
+
{env.apiUrl}
+
theme
+
+ {resolved} (preference: {preference}) +
+
+
+ {PREFERENCES.map((option) => ( + + ))} +
+
+ ); +}; diff --git a/packages/web/src/app/index.css b/packages/web/src/app/index.css new file mode 100644 index 0000000..b37726b --- /dev/null +++ b/packages/web/src/app/index.css @@ -0,0 +1,75 @@ +:root { + color-scheme: light dark; + + /* Fallbacks until ThemeProvider sets the resolved tokens on . */ + --color-background: #ffffff; + --color-surface: #f4f5f7; + --color-text: #0b0c0f; + --color-textMuted: #5b6472; + --color-accent: #2f6fed; + --color-border: #e2e5ea; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: system-ui, -apple-system, sans-serif; + background: var(--color-background); + color: var(--color-text); + transition: + background 0.2s ease, + color 0.2s ease; +} + +.app { + max-width: 640px; + margin: 0 auto; + padding: 3rem 1.5rem; +} + +.app h1 { + color: var(--color-accent); + margin-bottom: 0.25rem; +} + +.app dl { + display: grid; + grid-template-columns: max-content 1fr; + gap: 0.5rem 1rem; + padding: 1rem; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 12px; +} + +.app dt { + color: var(--color-textMuted); +} + +.app dd { + margin: 0; + font-family: ui-monospace, monospace; +} + +.theme-switch { + display: flex; + gap: 0.5rem; + margin-top: 1.5rem; +} + +.theme-switch button { + padding: 0.5rem 1rem; + border-radius: 8px; + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text); + cursor: pointer; +} + +.theme-switch button[aria-pressed='true'] { + border-color: var(--color-accent); + color: var(--color-accent); +} diff --git a/packages/web/src/app/main.tsx b/packages/web/src/app/main.tsx new file mode 100644 index 0000000..85f3d9f --- /dev/null +++ b/packages/web/src/app/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { ThemeProvider } from '../shared/theme'; +import { App } from './App'; +import './index.css'; + +const container = document.getElementById('root'); +if (container === null) { + throw new Error('Root container #root not found'); +} + +createRoot(container).render( + + + + + , +); diff --git a/packages/web/src/shared/config/env.ts b/packages/web/src/shared/config/env.ts new file mode 100644 index 0000000..9166c1d --- /dev/null +++ b/packages/web/src/shared/config/env.ts @@ -0,0 +1,9 @@ +// Web runtime config. Vite inlines `import.meta.env.VITE_*` at build time. + +export interface WebEnv { + apiUrl: string; +} + +export const env: WebEnv = { + apiUrl: import.meta.env.VITE_API_URL ?? 'http://localhost:8080', +}; diff --git a/packages/web/src/shared/config/index.ts b/packages/web/src/shared/config/index.ts new file mode 100644 index 0000000..6d8db38 --- /dev/null +++ b/packages/web/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export { env } from './env'; +export type { WebEnv } from './env'; diff --git a/packages/web/src/shared/theme/ThemeProvider.tsx b/packages/web/src/shared/theme/ThemeProvider.tsx new file mode 100644 index 0000000..04d2dce --- /dev/null +++ b/packages/web/src/shared/theme/ThemeProvider.tsx @@ -0,0 +1,71 @@ +import { createContext, useCallback, useEffect, useMemo, useState } from 'react'; +import type { ReactElement, ReactNode } from 'react'; +import { themes } from './tokens'; +import type { ThemeName } from './tokens'; + +export type ThemePreference = ThemeName | 'system'; + +export interface ThemeContextValue { + preference: ThemePreference; + resolved: ThemeName; + setPreference: (preference: ThemePreference) => void; +} + +export const ThemeContext = createContext(null); + +const STORAGE_KEY = 'altricade.theme'; +const DARK_QUERY = '(prefers-color-scheme: dark)'; + +const readStoredPreference = (): ThemePreference => { + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark' || stored === 'system') { + return stored; + } + return 'system'; +}; + +const readSystemTheme = (): ThemeName => + window.matchMedia(DARK_QUERY).matches ? 'dark' : 'light'; + +const applyTheme = (theme: ThemeName): void => { + const root = document.documentElement; + root.dataset['theme'] = theme; + const colors: Record = themes[theme]; + for (const [token, value] of Object.entries(colors)) { + root.style.setProperty(`--color-${token}`, value); + } +}; + +export const ThemeProvider = ({ children }: { children: ReactNode }): ReactElement => { + const [preference, setPreferenceState] = useState(readStoredPreference); + const [systemTheme, setSystemTheme] = useState(readSystemTheme); + + useEffect(() => { + const media = window.matchMedia(DARK_QUERY); + const onChange = (): void => { + setSystemTheme(media.matches ? 'dark' : 'light'); + }; + media.addEventListener('change', onChange); + return () => { + media.removeEventListener('change', onChange); + }; + }, []); + + const resolved: ThemeName = preference === 'system' ? systemTheme : preference; + + useEffect(() => { + applyTheme(resolved); + }, [resolved]); + + const setPreference = useCallback((next: ThemePreference): void => { + window.localStorage.setItem(STORAGE_KEY, next); + setPreferenceState(next); + }, []); + + const value = useMemo( + () => ({ preference, resolved, setPreference }), + [preference, resolved, setPreference], + ); + + return {children}; +}; diff --git a/packages/web/src/shared/theme/index.ts b/packages/web/src/shared/theme/index.ts new file mode 100644 index 0000000..b5e4ea8 --- /dev/null +++ b/packages/web/src/shared/theme/index.ts @@ -0,0 +1,5 @@ +export { ThemeProvider } from './ThemeProvider'; +export { useTheme } from './useTheme'; +export { themes } from './tokens'; +export type { ThemeName } from './tokens'; +export type { ThemePreference, ThemeContextValue } from './ThemeProvider'; diff --git a/packages/web/src/shared/theme/tokens.ts b/packages/web/src/shared/theme/tokens.ts new file mode 100644 index 0000000..7494465 --- /dev/null +++ b/packages/web/src/shared/theme/tokens.ts @@ -0,0 +1,33 @@ +// Design tokens — the single source of truth for colors. No hard-coded colors +// live in feature/entity/widget code; everything reads these via CSS variables. + +export type ThemeName = 'light' | 'dark'; + +export interface ThemeColors { + background: string; + surface: string; + text: string; + textMuted: string; + accent: string; + border: string; + [token: string]: string; +} + +export const themes: Record = { + light: { + background: '#ffffff', + surface: '#f4f5f7', + text: '#0b0c0f', + textMuted: '#5b6472', + accent: '#2f6fed', + border: '#e2e5ea', + }, + dark: { + background: '#0b0c0f', + surface: '#15171c', + text: '#f4f5f7', + textMuted: '#9aa3b2', + accent: '#5b8bff', + border: '#242833', + }, +}; diff --git a/packages/web/src/shared/theme/useTheme.ts b/packages/web/src/shared/theme/useTheme.ts new file mode 100644 index 0000000..f4756cb --- /dev/null +++ b/packages/web/src/shared/theme/useTheme.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +import { ThemeContext } from './ThemeProvider'; +import type { ThemeContextValue } from './ThemeProvider'; + +export const useTheme = (): ThemeContextValue => { + const context = useContext(ThemeContext); + if (context === null) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; diff --git a/packages/web/src/vite-env.d.ts b/packages/web/src/vite-env.d.ts new file mode 100644 index 0000000..c57d674 --- /dev/null +++ b/packages/web/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 0000000..dd09d68 --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts new file mode 100644 index 0000000..90ac88c --- /dev/null +++ b/packages/web/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: 5173, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..95441f2 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3853 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + esbuild: '>=0.28.1' + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.6.0) + eslint: + specifier: ^10.6.0 + version: 10.6.0 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.6.0) + eslint-plugin-boundaries: + specifier: ^7.0.2 + version: 7.0.2(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0) + globals: + specifier: ^17.7.0 + version: 17.7.0 + prettier: + specifier: ^3.9.5 + version: 3.9.5 + turbo: + specifier: ^2.10.4 + version: 2.10.4 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.63.0 + version: 8.63.0(eslint@10.6.0)(typescript@5.9.3) + + packages/backend: + dependencies: + '@altricade/core': + specifier: workspace:^ + version: link:../core + '@fastify/cors': + specifier: ^11.3.0 + version: 11.3.0 + fastify: + specifier: ^5.10.0 + version: 5.10.0 + fastify-plugin: + specifier: ^6.0.0 + version: 6.0.0 + ioredis: + specifier: ^5.11.1 + version: 5.11.1 + kysely: + specifier: ^0.29.3 + version: 0.29.3 + minio: + specifier: ^8.0.7 + version: 8.0.7 + node-pg-migrate: + specifier: ^8.0.4 + version: 8.0.4(@types/pg@8.20.0)(pg@8.22.0) + pg: + specifier: ^8.22.0 + version: 8.22.0 + devDependencies: + '@types/node': + specifier: ^26.1.1 + version: 26.1.1 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.16)(tsx@4.23.0)(typescript@5.9.3) + tsx: + specifier: ^4.23.0 + version: 4.23.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/core: {} + + packages/desktop: + dependencies: + '@altricade/core': + specifier: workspace:^ + version: link:../core + + packages/mobile: + dependencies: + '@altricade/core': + specifier: workspace:^ + version: link:../core + + packages/web: + dependencies: + '@altricade/core': + specifier: workspace:^ + version: link:../core + react: + specifier: ^19.2.7 + version: 19.2.7 + react-dom: + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) + devDependencies: + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.0.3 + version: 6.0.3(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(tsx@4.23.0)) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.6.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.1.4 + version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(tsx@4.23.0) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@boundaries/elements@3.0.1': + resolution: {integrity: sha512-T53UueJVRIn1B2G5FWo6T3rqAA1aHcuypRweQcbW6Z/leUygsHW54Gezkm/8uxB9Fw8ewE+izfKKmA9XVv7HdQ==} + engines: {node: '>=18.18'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/cors@11.3.0': + resolution: {integrity: sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.1.0': + resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@turbo/darwin-64@2.10.4': + resolution: {integrity: sha512-m1MUEI4MJ69r5CwfMYxmHi0H0rrgiYCBOp0tgBZ9x/YVvOb5uu/lRIDyDwdtH054R2yWeQaIigUGu6aCX9f8cA==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.10.4': + resolution: {integrity: sha512-VQ1Yxs5zkPT+2z7t1P4mvn6JmcKLkOCAsPuK9XbOvuVj0DlTlETfIXNisX0771v/vTWHOQqiwoGi+TtAUq8efw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.10.4': + resolution: {integrity: sha512-IzV1QovmwX7mfGnVinmE++2IB8tbeo38weltiuH5zNqwCTBjLs/DytyRKx+bmnhHdXIq9SheR8p0Nip/LBUPHg==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.10.4': + resolution: {integrity: sha512-rfujSQkP5aYiRn0PgTM7F00WkJCP/bKDVZbOx3WmrZwa/vHA0bplhCl328kpX7VI9HH2vI90ISGwuSVgJgoqTw==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.10.4': + resolution: {integrity: sha512-NnspP7Wd5fa3Wwnqv9bKfhegqZzuHBgbPxdZU/idTLQcazx/vgKu95JlCx2YHY0hdvKCnPcARrDwM+KEUmaO7A==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.10.4': + resolution: {integrity: sha512-Iv02YgOpaEShc2OkG7mgCJ2pEw1RUKiKbs0h8W5wAf4jZ5vpmraTEjuGTgHRuOORQnC1GN3KHo5WB+hu1abRMA==} + cpu: [arm64] + os: [win32] + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@typescript-eslint/eslint-plugin@8.63.0': + resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.63.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.63.0': + resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.63.0': + resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.63.0': + resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.63.0': + resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.63.0': + resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.63.0': + resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.63.0': + resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.63.0': + resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.63.0': + resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.3': + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + + browserslist@4.28.5: + resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.28.1' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001803: + resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-boundaries@7.0.2: + resolution: {integrity: sha512-VLaPMvNh+ONw6F/S0gpkS0/QDudqVjcaL1DoGo+8sJqZmxxabOtrFZ24PDI1jQLg3pyqujzJ3/4Cq7Bk249gjQ==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@7.0.1: + resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fast-uri@4.1.0: + resolution: {integrity: sha512-ZodJ2cRiLVWGi9IgPb3mbgSqM4CD3LexCHkuv0FfBXHJI1ADfucTD06m6clO2Cy5RZYsw/SiCVl/dyrFI/SYWA==} + + fast-xml-builder@1.2.1: + resolution: {integrity: sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==} + + fast-xml-parser@5.9.3: + resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} + hasBin: true + + fastify-plugin@6.0.0: + resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} + + fastify@5.10.0: + resolution: {integrity: sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + find-my-way@9.6.0: + resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} + engines: {node: '>=20'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + engines: {node: '>=18'} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + + ipaddr.js@2.4.0: + resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} + engines: {node: '>= 10'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-unsafe@1.0.1: + resolution: {integrity: sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kysely@0.29.3: + resolution: {integrity: sha512-VHtBdW6XB/pgoTSqraM3UAa2rYoYdNXqnNPpX+8XXP+cwYbVEFuAp3HyPt1vpNfU9l7Y2kpUrA9QDPsy8uUqOQ==} + engines: {node: '>=22.0.0'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minio@8.0.7: + resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==} + engines: {node: ^16 || ^18 || >=20} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-pg-migrate@8.0.4: + resolution: {integrity: sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==} + engines: {node: '>=20.11.0'} + hasBin: true + peerDependencies: + '@types/pg': '>=6.0.0 <9.0.0' + pg: '>=4.3.0 <9.0.0' + peerDependenciesMeta: + '@types/pg': + optional: true + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + engines: {node: '>=14'} + hasBin: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.23.0: + resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==} + engines: {node: '>=18.0.0'} + hasBin: true + + turbo@2.10.4: + resolution: {integrity: sha512-GQpduILaKjoaGljw097ScsSyKTtZSY7cZ3bJktzfTkPMyCf3ShKLuXK2IaOEN2Plziml+ArR7WJ1m+V4VbnaKQ==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.63.0: + resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: '>=0.28.1' + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@boundaries/elements@3.0.1(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)': + dependencies: + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.6.0) + handlebars: 4.7.9 + is-core-module: 2.16.1 + micromatch: 4.0.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + dependencies: + eslint: 10.6.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.6.0)': + optionalDependencies: + eslint: 10.6.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 3.1.3 + + '@fastify/cors@11.3.0': + dependencies: + fastify-plugin: 6.0.0 + toad-cache: 3.7.4 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.1.0': + dependencies: + fast-json-stringify: 7.0.1 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.4.0 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@ioredis/commands@1.10.0': {} + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nodable/entities@2.2.0': {} + + '@oxc-project/types@0.139.0': {} + + '@pinojs/redact@0.4.0': {} + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@turbo/darwin-64@2.10.4': + optional: true + + '@turbo/darwin-arm64@2.10.4': + optional: true + + '@turbo/linux-64@2.10.4': + optional: true + + '@turbo/linux-arm64@2.10.4': + optional: true + + '@turbo/windows-64@2.10.4': + optional: true + + '@turbo/windows-arm64@2.10.4': + optional: true + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.1.1': + dependencies: + undici-types: 8.3.0 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 26.1.1 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.63.0 + eslint: 10.6.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.63.0 + debug: 4.4.3 + eslint: 10.6.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.63.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.63.0': + dependencies: + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 + + '@typescript-eslint/tsconfig-utils@8.63.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.63.0(eslint@10.6.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.6.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.63.0': {} + + '@typescript-eslint/typescript-estree@8.63.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.63.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.63.0(eslint@10.6.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + eslint: 10.6.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.63.0': + dependencies: + '@typescript-eslint/types': 8.63.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.3(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(tsx@4.23.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(tsx@4.23.0) + + abstract-logging@2.0.1: {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anynum@1.0.1: {} + + async@3.2.6: {} + + atomic-sleep@1.0.0: {} + + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.42: {} + + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-or-node@2.1.1: {} + + browserslist@4.28.5: + dependencies: + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001803 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.5) + + buffer-crc32@1.0.0: {} + + bundle-require@5.1.0(esbuild@0.28.1): + dependencies: + esbuild: 0.28.1 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + caniuse-lite@1.0.30001803: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cluster-key-slot@1.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-uri-component@0.2.2: {} + + deep-is@0.1.4: {} + + denque@2.1.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.389: {} + + emoji-regex@8.0.0: {} + + es-errors@1.3.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.6.0): + dependencies: + eslint: 10.6.0 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.2 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.6.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + eslint: 10.6.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-boundaries@7.0.2(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0): + dependencies: + '@boundaries/elements': 3.0.1(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0) + chalk: 4.1.2 + eslint: 10.6.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.6.0) + handlebars: 4.7.9 + micromatch: 4.0.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-react-hooks@7.1.1(eslint@10.6.0): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 10.6.0 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.6.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter3@5.0.4: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@7.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.3: {} + + fast-uri@4.1.0: {} + + fast-xml-builder@1.2.1: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.1.0 + + fast-xml-parser@5.9.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.1 + is-unsafe: 1.0.1 + path-expression-matcher: 1.6.2 + strnum: 2.4.1 + xml-naming: 0.1.0 + + fastify-plugin@6.0.0: {} + + fastify@5.10.0: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.1.0 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 7.0.1 + find-my-way: 9.6.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + find-my-way@9.6.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.62.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + globals@17.7.0: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ipaddr.js@2.4.0: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.4 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-unsafe@1.0.1: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + joycon@3.1.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kysely@0.29.3: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash@4.18.1: {} + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimist@1.2.8: {} + + minio@8.0.7: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 1.0.0 + eventemitter3: 5.0.4 + fast-xml-parser: 5.9.3 + ipaddr.js: 2.4.0 + lodash: 4.18.1 + mime-types: 2.1.35 + query-string: 7.1.3 + stream-json: 1.9.1 + through2: 4.0.2 + xml2js: 0.6.2 + + minipass@7.1.3: {} + + mlly@1.8.2: + dependencies: + acorn: 8.17.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.15: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-pg-migrate@8.0.4(@types/pg@8.20.0)(pg@8.22.0): + dependencies: + glob: 11.1.0 + pg: 8.22.0 + yargs: 17.7.3 + optionalDependencies: + '@types/pg': 8.20.0 + + node-releases@2.0.51: {} + + object-assign@4.1.1: {} + + on-exit-leak-free@2.1.2: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + path-exists@4.0.0: {} + + path-expression-matcher@1.6.2: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.16)(tsx@4.23.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.16 + tsx: 4.23.0 + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prelude-ls@1.2.1: {} + + prettier@3.9.5: {} + + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + punycode@2.3.1: {} + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + quick-format-unescaped@4.0.4: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react@19.2.7: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + sax@1.6.0: {} + + scheduler@0.27.0: {} + + secure-json-parse@4.1.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + split-on-first@1.1.0: {} + + split2@4.2.0: {} + + standard-as-callback@2.1.0: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toad-cache@3.7.4: {} + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: + optional: true + + tsup@8.5.1(postcss@8.5.16)(tsx@4.23.0)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.28.1) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.28.1 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.16)(tsx@4.23.0) + resolve-from: 5.0.0 + rollup: 4.62.2 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.16 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.23.0: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + turbo@2.10.4: + optionalDependencies: + '@turbo/darwin-64': 2.10.4 + '@turbo/darwin-arm64': 2.10.4 + '@turbo/linux-64': 2.10.4 + '@turbo/linux-arm64': 2.10.4 + '@turbo/windows-64': 2.10.4 + '@turbo/windows-arm64': 2.10.4 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.63.0(eslint@10.6.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@5.9.3) + eslint: 10.6.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + uglify-js@3.19.3: + optional: true + + undici-types@8.3.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.5): + dependencies: + browserslist: 4.28.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(tsx@4.23.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.1.1 + esbuild: 0.28.1 + fsevents: 2.3.3 + tsx: 4.23.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + xml-naming@0.1.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..18ec407 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/specs.md b/specs.md new file mode 100644 index 0000000..0cb515d --- /dev/null +++ b/specs.md @@ -0,0 +1,417 @@ +# Project Spec — Realtime Chat & Calls App + +> This document is a brief for Claude Code (plan mode). Read it fully, then propose a +> plan before writing any code. Build the skeleton first, then implement part by part +> with check-ins. Do NOT scaffold the whole thing in one pass. See "How to work" (section 11) +> — it is as important as the architecture. + +--- + +## 1. What we are building + +A self-hosted, cross-platform messaging app with: + +- **Production-grade auth**: register, login, token refresh, logout, with username, + display name, avatar, and optional phone number — a Telegram-style profile model + (see section 4). +- **1:1 direct messages and group rooms** (Discord/Slack-shaped). +- **Media messages**: voice notes, video messages, images (files, not streamed). +- **Live features**: typing indicators, online/presence, last-seen, read receipts, + edits, reactions — all delivered in realtime. +- **Voice/video calls with screen share** — group conferences and 1:1. **This is a LATER + phase (Phase 8). Do not scaffold call/WebRTC code until the chat core is working and I + say so.** + +Targets: web, iOS, Android, and desktop (macOS + Windows). + +Everything server-side runs in **Docker Compose** — the backend included, not just the +supporting services. See section 9. + +## 2. Core mental model (read this before anything else) + +The realtime transport (**Centrifugo**) is a **dumb, fast pipe**, NOT the chat server. +The **backend is the brain**. + +**Sending is over REST; receiving is over the WebSocket.** This split is deliberate and is +the most common thing people get wrong — do not "optimize" it away: + +1. **Send** (client → server): a normal REST POST to the backend. The client's realtime + socket is effectively **receive-only**. Sending must go through the backend because + that is the only place validation, membership checks, persistence, server-assigned IDs, + ordering, and rate-limiting can happen. +2. The backend authorizes, writes to Postgres (source of truth), then calls Centrifugo's + HTTP API to publish into the channel. +3. **Receive** (server → client): Centrifugo fans the event out over the WebSocket to all + subscribers — including the original sender, so all their devices stay in sync and they + get the same server-assigned ID and ordering as everyone else. The sender renders the + message from the socket echo, NOT from the REST response. + +Do NOT let clients publish chat messages directly through Centrifugo, and do NOT tunnel +sends through Centrifugo RPC — a plain REST POST reaches the same backend endpoint more +simply, and send latency is dominated by the DB write anyway. + +### Channel model + +- `room:` — one channel per room; all members subscribe. +- `user:` — one **personal channel** per user. DMs are delivered by publishing the + message into BOTH participants' personal channels; the client routes by `conversationId` + in the payload. Scales to one subscription per user regardless of conversation count, and + doubles as the notification / unread / "added to room" / read-receipt channel. + +### Two auth gates (keep them separate) — see section 4 for the full auth design + +- **Connection auth**: backend mints a Centrifugo JWT (shared secret with Centrifugo) after + the user is authenticated; Centrifugo verifies the signature — no backend call per + connection. This is a SEPARATE token from the app's own access token. +- **Subscription auth**: use the **subscribe-proxy** pattern — Centrifugo calls a backend + endpoint on each subscribe to check Postgres membership ("can user U join channel C?"). + Reflects current DB state (kicked users lose access immediately). Personal-channel + subscriptions are authorized by matching the token's user id. + +### Boundaries to respect + +- Centrifugo history is a **short recovery buffer** (reconnect replay), NOT the message + store. Load conversation history from Postgres via REST. +- **Server-assigned monotonic message IDs + ordering** and **client-side dedupe** are + required from day one (messages arrive both live and via history load, and can overlap). +- On reconnect: subscribe FIRST, buffer incoming live events, THEN load history via REST, + then merge + dedupe. Ordering of these two steps prevents "missing message" gaps. +- Media (voice notes / video messages / images) upload to object storage (MinIO) via the + backend (presigned URLs); only a **reference** (URL + metadata) is published through the + channel. Never push binary through Centrifugo. + +## 3. Event taxonomy (how every realtime feature is handled) + +Every live feature is just another event on the same receive socket. There is no separate +system for typing vs presence vs messages — you publish different event types onto the same +channels and the one socket carries them all. Each event falls into exactly one bucket: + +### Bucket A — Durable + must be correct → through the backend (persist, then publish) + +The "brain" rule applies: REST in, backend validates + writes to Postgres, backend +publishes to the relevant channel, clients receive over the socket. + +- `message.new` — a new chat message. +- `message.edit` — edited message (store edit + edited_at). +- `message.delete` — soft delete. +- `reaction.add` / `reaction.remove` — emoji reactions. +- `read.receipt` — "user X has read up to message N" (updates `read_state`, published to + conversation participants so checkmarks update live). +- `last_seen` — timestamp written to Postgres on disconnect or periodically; loaded via + REST with conversation metadata. Durable because it must survive restarts and be queryable. +- `room.membership` — added to / removed from a room (published to the affected user's + personal channel). +- `profile.update` — display name / avatar / username changes (published to that user's + personal channel so contacts see the update live). + +### Bucket B — Ephemeral + throwaway → published to the channel, NOT persisted + +No database write. Backend involvement optional (a tiny fire-and-forget REST call, or a +constrained client-side publish, is acceptable here precisely because there is nothing to +validate or store — worst case a spurious flicker). + +- `typing.start` / `typing.stop` — published to `room:` or the DM's channels; + auto-clears on the client after ~3s if no follow-up. High-frequency, never stored. +- `recording.audio` / `recording.video` — "recording a voice note…" style indicators. + +### Bucket C — Connection-derived → Centrifugo built-in presence, no code of your own + +- `presence` / online status — use Centrifugo's built-in channel presence (join/leave + events, "who is subscribed"). Room online lists come from presence on `room:`; a + user's global online dot is derived from whether they hold an active `user:` + connection. Subscribe to presence updates on the socket. + +**Rule of thumb for any future feature:** if it must be correct and durable → Bucket A. If +it's throwaway → Bucket B. If it's "who's connected" → Bucket C. Define the event type, +publish it, handle it on the client — never stand up new infrastructure. + +All event type names and payload shapes live in the shared `core` package (section 6). + +## 4. Auth & user identity (production-grade — Telegram-style profile) + +Auth is security-critical and hard to change later. Use vetted, well-established +building blocks — DO NOT hand-roll crypto, hashing, or token logic. Where a mature library +exists (password hashing, JWT handling), use it. + +### Identity / profile model + +- **username** — unique, immutable-ish public handle (like Telegram @handle). Case-insensitive + unique. Used for search / mentions / DM initiation. Required. +- **display_name** — freely editable, non-unique, shown in UI. Required. +- **avatar** — optional image, uploaded to MinIO, stored as a reference (URL + metadata), + same upload path as media messages. Changing it emits `profile.update` (Bucket A). +- **phone number** — OPTIONAL (like Telegram, not required to have an account). If present, + stored normalized (E.164). May be used later for contact discovery; keep it optional and + privacy-controlled from day one. +- **email** — decide in planning whether email is the primary credential or optional. Default + assumption: email OR username + password for login; phone optional and additive. Confirm + with me before locking this. + +### Credentials & registration + +- **Register**: username + display_name + password (+ optional email/phone). Enforce + username uniqueness and a sane password policy (length-based, not silly complexity rules). +- **Password hashing**: use **argon2id** (preferred) or bcrypt via a maintained library. + Never store plaintext, never invent a hashing scheme, never use fast hashes (MD5/SHA). +- **Login**: credential + password → issue an access token + refresh token (see below). +- Rate-limit login and register endpoints to blunt credential stuffing / enumeration. + Return non-enumerating errors ("invalid credentials", not "no such user"). + +### Token model (this is the production-grade part — get it right) + +Two app tokens, plus the separate Centrifugo token: + +- **Access token** — short-lived JWT (e.g. ~15 min), sent on every REST request + (Authorization: Bearer). Stateless; carries user id + minimal claims. Never long-lived. +- **Refresh token** — long-lived, **opaque** (random, not a JWT), stored server-side + (hashed) in a `refresh_tokens` table, one row per device/session. Used only at the + `/auth/refresh` endpoint to mint a new access token. +- **Refresh rotation with reuse detection**: every refresh issues a NEW refresh token and + invalidates the old one. If an already-used (rotated-out) refresh token is presented + again, treat it as theft — revoke the whole token family / session and force re-login. + This is a required behavior, not optional. +- **Centrifugo connection token** — a SEPARATE short-lived JWT minted by the backend + (signed with the Centrifugo shared secret) once the user is authenticated. Do not reuse + the app access token for Centrifugo. The client fetches/refreshes it via an authenticated + backend endpoint. + +### Sessions & devices + +- `refresh_tokens` (or `sessions`) table tracks each active session: user_id, hashed token, + device/user-agent info, created_at, last_used_at, expires_at, revoked_at. +- **Logout** revokes the current session's refresh token. Support "log out this device" and + "log out all devices" (revoke all rows for the user). +- Multi-device is expected (Telegram-style): a user may have several active sessions at once; + the personal-channel model already supports fan-out to all their devices. + +### Endpoints (starting set — refine in planning) + +`POST /auth/register`, `POST /auth/login`, `POST /auth/refresh`, `POST /auth/logout`, +`POST /auth/logout-all`, `GET /me`, `PATCH /me` (display_name / avatar / phone), +`GET /auth/sessions`, `POST /auth/centrifugo-token`, `GET /users/:username` (public profile), +avatar upload endpoint (presigned, via the media path). + +### Security requirements (non-negotiable) + +- Passwords hashed with argon2id/bcrypt via a library; never logged, never returned. +- Refresh tokens opaque + hashed at rest + rotated + reuse-detected. +- Access tokens short-lived; secrets from env (section 9), never committed. +- Rate-limit auth endpoints; non-enumerating error messages. +- Validate all inputs against JSON Schema from `core`. +- Treat the Centrifugo token as separate and short-lived. +- DO NOT hand-roll cryptography or token schemes — use maintained libraries and standard + patterns. Flag to me if a requirement seems to need custom crypto (it almost never does). + +## 5. Stack (decided — do not re-litigate) + +- **Language everywhere:** TypeScript. +- **Monorepo:** single repo, pnpm workspaces. (NOT separate repos — shared code is imported + by source so type changes surface as immediate compile errors.) +- **Backend framework:** Fastify. (NestJS considered and rejected: too much ceremony/DI for + a small team and a realtime/event-shaped workload.) +- **Auth libraries:** argon2 (or bcrypt) for hashing; a maintained JWT library for access + tokens; opaque random refresh tokens stored hashed. No custom crypto. +- **Validation:** JSON Schema via Fastify's built-in support, sourced from `core`. +- **Realtime transport:** Centrifugo (self-hosted, runs in Docker Compose). +- **DB:** Postgres 16 (source of truth). **Redis** for Centrifugo scaling + history/recovery + (and available for auth rate-limiting / token-family tracking if useful). +- **Object storage:** MinIO (S3-compatible) — media messages AND avatars. +- **Web frontend:** React + TypeScript + Vite. +- **Mobile frontend:** React Native + Expo + TypeScript. +- **Desktop:** thin Tauri (preferred) or Electron shell wrapping the web build. +- **Calls (LATER, Phase 8):** LiveKit (SFU) + coturn (TURN over TLS/443). Not now. + +## 6. Monorepo layout + +``` +/ +├── package.json # pnpm workspace root +├── pnpm-workspace.yaml +├── tsconfig.base.json +├── docker-compose.yml # base: all backend-side services (see section 9) +├── docker-compose.override.yml # local dev conveniences (hot reload, exposed ports) +├── docker-compose.prod.yml # deploy overrides (restart policies, no source mounts) +├── .env.example # every env var documented; real .env is gitignored +├── packages/ +│ ├── core/ # THE KEYSTONE — platform-agnostic, imported by all +│ │ ├── src/ +│ │ │ ├── types/ # message/event/API payload types + user/profile types +│ │ │ ├── events/ # event type constants + payloads (section 3 taxonomy) +│ │ │ ├── schemas/ # JSON Schemas (backend validates, clients type off these) +│ │ │ ├── channels/ # channel-name builders: room:, user: +│ │ │ ├── realtime/ # Centrifugo SDK wrapper (UI-agnostic) +│ │ │ ├── api/ # typed API client functions (incl. auth flows) +│ │ │ └── messages/ # dedupe + ordering logic +│ │ └── package.json +│ ├── backend/ # Fastify service; imports core; runs in Docker +│ │ ├── src/ +│ │ │ ├── routes/ # auth, users/profile, messages, rooms, conversations, media, subscribe-proxy +│ │ │ ├── plugins/ # db, redis, centrifugo client, auth/jwt, rate-limit +│ │ │ ├── auth/ # hashing, token issue/rotate/verify, session management +│ │ │ ├── db/ # migrations + queries +│ │ │ └── server.ts +│ │ ├── Dockerfile # multi-stage; see section 9 +│ │ └── package.json +│ ├── web/ # React + Vite; imports core +│ │ ├── Dockerfile # optional: containerized static build for deploy +│ │ └── package.json +│ ├── mobile/ # React Native + Expo; imports core (not containerized) +│ │ └── package.json +│ └── desktop/ # Tauri/Electron shell over web build (not containerized) +│ └── package.json +├── infra/ +│ ├── centrifugo/config.json # shared JWT secret, subscribe proxy, namespaces +│ ├── postgres/init/ # init SQL if needed +│ └── nginx/ # reverse proxy config (routes /api and /connection/websocket) +└── PROJECT_SPEC.md +``` + +### Rules for `core` + +- No `react-dom` or `react-native` imports anywhere in `core`. It is pure TS. +- Keep React hooks / state logic UI-agnostic where possible so it can migrate into `core` + later. Do NOT bake DOM/RN assumptions into the Centrifugo/API wrappers. +- One source of truth for payload shapes, event definitions, AND auth/profile types: all + live here; backend and clients both consume them. + +## 7. Data model (starting point — refine in planning) + +Tables (Postgres): `users`, `refresh_tokens` (aka `sessions`), `rooms`, `room_members`, +`conversations` (for DMs), `messages`, `reactions`, `read_state`. + +- `users`: id, username (unique, case-insensitive), display_name, avatar_ref (nullable), + phone (nullable, E.164), email (nullable or primary — confirm in planning), + password_hash, created_at, last_seen_at. +- `refresh_tokens`: id, user_id, token_hash, device/user-agent, created_at, last_used_at, + expires_at, revoked_at, family/rotation id for reuse detection. +- `messages`: **server-assigned monotonic id/sequence** (BIGSERIAL or per-room counter) + + a stable id for client-side dedupe. +- Membership drives channel access (the subscribe-proxy checks these tables). +- Typing/presence/recording are NOT tables (Buckets B and C — not persisted). + +## 8. Build phases (implement in this order) + +**Phase 0 — Skeleton + Docker.** Monorepo tooling, workspaces, tsconfig, empty packages that +build and import `core`. Full `docker-compose.yml` bringing up Centrifugo + Postgres + Redis + +- MinIO + backend, with the dev override for hot reload. No features yet. Verify the whole + stack boots with one `docker compose up` and the backend reaches every service. + +**Phase 1 — Auth (production-grade).** Implement section 4 in full: register, login, refresh +with rotation + reuse detection, logout / logout-all, sessions table, argon2 hashing, access + +- refresh tokens, `/me` + `PATCH /me`, rate-limiting on auth endpoints. This is the + foundation everything else authenticates against — get it solid before features. + +**Phase 2 — Realtime connection.** Backend mints the separate Centrifugo token; web client +connects its (receive-only) socket using it and subscribes to its personal channel via the +subscribe-proxy. Prove authenticated end-to-end connectivity. + +**Phase 3 — Rooms + messaging.** Room CRUD, membership, subscribe-proxy authorization, +send-message REST endpoint → persist → publish to `room:`. History load from Postgres. +Server-assigned ids + client dedupe. `message.new` end to end. + +**Phase 4 — DMs.** Personal-channel delivery (publish to both participants), conversation +model, client routing by `conversationId`. + +**Phase 5 — Live features (event taxonomy).** Typing (Bucket B), presence (Bucket C, +built-in), last-seen (Bucket A), read receipts (Bucket A), edits + reactions (Bucket A), +profile.update (Bucket A). Implement per the section-3 buckets. + +**Phase 6 — Media + avatars.** MinIO presigned uploads; voice notes / video messages / +images as references published through channels; user avatars via the same path. + +**Phase 7 — Mobile + desktop clients.** React Native + Expo consuming the same `core` (reuse +realtime + API + auth layer; platform-specific UI only). Then the Tauri/Electron desktop +shell over the web build. + +**Phase 8 (LATER, separate effort) — Calls.** LiveKit + coturn (added as new Docker Compose +services), call signaling over Centrifugo (ring via personal channel), token minting in +backend, screen share. Do NOT start until explicitly told. TURN-over-443 + simulcast are +legitimate reliability engineering, NOT censorship circumvention (see section 12). + +## 9. Docker & deployment (IMPORTANT — Docker Compose is the primary runtime) + +Everything server-side runs in Docker Compose, for BOTH local development and deploy. The +backend is containerized too — do not assume it runs on the host. Aim for a single +`docker compose up` to bring up the entire backend stack locally. + +### Compose file strategy (use overrides, don't fork the file) + +- `docker-compose.yml` — base definition of all services: `backend`, `centrifugo`, + `postgres`, `redis`, `minio`, `nginx`. Sensible defaults, named volumes, a private + network, healthchecks, and `depends_on` with `condition: service_healthy` so the backend + waits for Postgres/Redis to be ready. +- `docker-compose.override.yml` — auto-loaded in local dev: mount backend source for hot + reload (e.g. `tsx watch` / `nodemon`), expose service ports to the host, use dev secrets, + seed data. Convenience only. +- `docker-compose.prod.yml` — explicit for deploy: `restart: unless-stopped`, NO source + mounts (use the built image), secrets from real env, tighter exposure (only nginx faces + the internet). Deploy with + `docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d`. + +### Backend Dockerfile + +- **Multi-stage**: a build stage (install pnpm, build `core` + `backend`) and a slim runtime + stage (node:LTS-alpine or distroless, production deps only). The monorepo means the build + stage must handle pnpm workspaces — build `core` first, then `backend`. +- Run as a **non-root** user. Add a `HEALTHCHECK` hitting a `/health` endpoint. +- `.dockerignore` to keep `node_modules`, build artifacts, and other packages out of context. + +### Services — requirements + +- **backend**: depends_on postgres+redis (healthy), reads all config from env, exposes only + to the internal network in prod (nginx proxies it). `/health` and `/ready` endpoints. + Runs DB migrations on startup or via a one-shot migration service. +- **centrifugo**: official image, config from `infra/centrifugo/config.json` + env. Shared + JWT secret via env, subscribe-proxy endpoint pointing at the backend service name, `room:` + and `user:` namespaces defined, Redis engine + history enabled for recovery. **Raise the + `nofile` ulimit** (e.g. 65536) — FD exhaustion is the first wall for a realtime server. +- **postgres**: version 16, named volume, healthcheck (`pg_isready`), init SQL mount if + useful. Never expose to the internet in prod. +- **redis**: Centrifugo scaling + history/recovery; present from Phase 0 as the coordination + layer future clustering depends on. Named volume if persistence wanted. +- **minio**: S3-compatible object storage for media + avatars; console + API, named volume, + bucket bootstrap on startup. +- **nginx**: reverse proxy / TLS termination. Routes `/api/*` → backend and + `/connection/websocket` → centrifugo, passing WebSocket upgrade headers correctly. In prod + this is the only internet-facing service. + +### General Docker rules + +- Secrets via env / `.env` (gitignored) — never baked into images or committed. Ship a fully + documented `.env.example` (include all auth secrets: JWT signing key, Centrifugo shared + secret, DB/Redis/MinIO creds). +- Named volumes for all stateful services (postgres, redis, minio) so data survives + `docker compose down`. +- Private network for inter-service traffic; only nginx published to the host in prod. +- Pin image versions (no bare `latest`) for reproducibility. +- Every long-running service has a healthcheck; the backend uses them via `depends_on`. +- Local dev must be one command. Document it in the README the scaffold generates. + +## 10. Scaling assumptions (design for, don't build yet) + +- Backend and Centrifugo are **stateless** — no in-memory session state (sessions live in + Postgres/Redis, not process memory). This is what lets them scale horizontally later + (multiple backend and Centrifugo replicas behind nginx, coordinated by Redis). +- Media plane (LiveKit/coturn, Phase 8) stays **physically separate** from the control plane. +- Redis present from Phase 0 (coordination layer for later Centrifugo + LiveKit clustering). + +## 11. How to work (IMPORTANT — follow this) + +- **Plan first.** Before writing code, produce a plan for the current phase only and wait for + my approval. Do not jump ahead to later phases. +- **Explain before implementing.** For each phase, briefly explain the approach and the + reasoning behind key decisions before generating code. I prefer understanding the "why". +- **Go part by part with check-ins.** Implement one coherent piece, stop, let me review, then + continue. Do not dump large amounts of generated code in one go. +- **Do not add features I didn't ask for.** No calls/WebRTC before Phase 8. No speculative + abstractions. +- **Ask when a decision is ambiguous** rather than guessing, especially the data model, API + shapes, event payloads, and the email-vs-username-vs-phone credential question in section 4. +- **Respect the decided stack** in section 5 — don't substitute frameworks or add heavy + dependencies without flagging why. +- **For auth, use vetted libraries and standard patterns — never hand-roll crypto or token + logic.** Flag anything that seems to need custom crypto. +- Keep the shared `core` package clean of platform-specific imports. +- Prefer the Docker Compose workflow (section 9) for anything that touches running services. diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..09796d4 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "useUnknownInCatchVariables": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + + "verbatimModuleSyntax": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + + "sourceMap": true, + "noEmit": true, + + "skipLibCheck": true + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..154364e --- /dev/null +++ b/turbo.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "stream", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "typecheck": { + "dependsOn": ["^build"] + }, + "lint": { + "dependsOn": ["^build"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +}