From f11ea58c2cc5ff525b16da87494fd8b6fc1ffb71 Mon Sep 17 00:00:00 2001 From: Millaguie Date: Tue, 16 Jun 2026 10:27:33 +0200 Subject: [PATCH] Sternboard: free retro board, no signup (initial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Servidor Fastify + Socket.IO + Drizzle/Postgres; front Vite+React (EN/ES, tema claro/oscuro). Tableros por columnas con plantillas, notas privadas/públicas, agrupación, votos configurables (multivoto), timer compartido con alarma, fases, action items, export Markdown/JSON e import, autodestrucción, gestión de columnas. Imagen única (front servido por el servidor) y CI que la construye y publica. --- .dockerignore | 9 + .gitea/workflows/ci.yml | 68 + .gitignore | 13 + Dockerfile | 27 + README.md | 55 + docker-compose.yml | 34 + e2e/global-setup.ts | 61 + e2e/package-lock.json | 91 + e2e/package.json | 13 + e2e/playwright.config.ts | 43 + e2e/tests/columns.spec.ts | 40 + e2e/tests/groups.spec.ts | 99 + e2e/tests/import-export.spec.ts | 52 + e2e/tests/panels.spec.ts | 92 + e2e/tests/retro.spec.ts | 123 + server/.env.example | 3 + server/drizzle.config.ts | 10 + server/package-lock.json | 4426 ++++++++++++++++++++++ server/package.json | 29 + server/src/app.ts | 278 ++ server/src/db/index.ts | 12 + server/src/db/schema.ts | 95 + server/src/env.ts | 10 + server/src/index.ts | 7 + server/src/markdown.ts | 85 + server/src/room.ts | 102 + server/src/socket.ts | 591 +++ server/src/templates.ts | 48 + server/test/cleanup.test.ts | 48 + server/test/columns.test.ts | 72 + server/test/global-setup.ts | 96 + server/test/groups.test.ts | 73 + server/test/helpers.ts | 85 + server/test/import.test.ts | 101 + server/test/multivote.test.ts | 62 + server/test/options.test.ts | 119 + server/test/private.test.ts | 59 + server/test/rest.test.ts | 58 + server/test/reveal-votes.test.ts | 73 + server/test/socket.test.ts | 108 + server/test/truncate.ts | 9 + server/tsconfig.json | 14 + server/vitest.config.ts | 15 + web/index.html | 12 + web/package-lock.json | 2074 ++++++++++ web/package.json | 26 + web/src/components/ActionPointsPanel.tsx | 91 + web/src/components/AppBar.tsx | 19 + web/src/components/Drawer.tsx | 25 + web/src/components/ExportModal.tsx | 96 + web/src/components/ParticipantsPanel.tsx | 91 + web/src/components/SettingsPanel.tsx | 132 + web/src/i18n.tsx | 308 ++ web/src/identity.ts | 32 + web/src/main.tsx | 23 + web/src/pages/Board.tsx | 1031 +++++ web/src/pages/Home.tsx | 242 ++ web/src/sound.ts | 44 + web/src/styles.css | 384 ++ web/src/theme.tsx | 50 + web/src/types.ts | 39 + web/src/useBoard.ts | 105 + web/tsconfig.json | 14 + web/vite.config.ts | 13 + 64 files changed, 12359 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 e2e/global-setup.ts create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/tests/columns.spec.ts create mode 100644 e2e/tests/groups.spec.ts create mode 100644 e2e/tests/import-export.spec.ts create mode 100644 e2e/tests/panels.spec.ts create mode 100644 e2e/tests/retro.spec.ts create mode 100644 server/.env.example create mode 100644 server/drizzle.config.ts create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/src/app.ts create mode 100644 server/src/db/index.ts create mode 100644 server/src/db/schema.ts create mode 100644 server/src/env.ts create mode 100644 server/src/index.ts create mode 100644 server/src/markdown.ts create mode 100644 server/src/room.ts create mode 100644 server/src/socket.ts create mode 100644 server/src/templates.ts create mode 100644 server/test/cleanup.test.ts create mode 100644 server/test/columns.test.ts create mode 100644 server/test/global-setup.ts create mode 100644 server/test/groups.test.ts create mode 100644 server/test/helpers.ts create mode 100644 server/test/import.test.ts create mode 100644 server/test/multivote.test.ts create mode 100644 server/test/options.test.ts create mode 100644 server/test/private.test.ts create mode 100644 server/test/rest.test.ts create mode 100644 server/test/reveal-votes.test.ts create mode 100644 server/test/socket.test.ts create mode 100644 server/test/truncate.ts create mode 100644 server/tsconfig.json create mode 100644 server/vitest.config.ts create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/src/components/ActionPointsPanel.tsx create mode 100644 web/src/components/AppBar.tsx create mode 100644 web/src/components/Drawer.tsx create mode 100644 web/src/components/ExportModal.tsx create mode 100644 web/src/components/ParticipantsPanel.tsx create mode 100644 web/src/components/SettingsPanel.tsx create mode 100644 web/src/i18n.tsx create mode 100644 web/src/identity.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/Board.tsx create mode 100644 web/src/pages/Home.tsx create mode 100644 web/src/sound.ts create mode 100644 web/src/styles.css create mode 100644 web/src/theme.tsx create mode 100644 web/src/types.ts create mode 100644 web/src/useBoard.ts create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d6e9f7b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +**/node_modules +**/dist +**/.env +**/*.log +e2e +.git +.gitea +**/.claude +server/drizzle diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..8d6ff07 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI/CD Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REGISTRY: tea.millaguie.net + IMAGE_NAME: ${{ gitea.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Server — install & typecheck + working-directory: server + run: | + npm install + npx tsc --noEmit + + - name: Web — install, typecheck & build + working-directory: web + run: | + npm install + npm run build + + build-and-push: + runs-on: ubuntu-latest + needs: test + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,prefix= + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + target: production + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d536e18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +.env +*.log + +# Playwright +e2e/test-results/ +e2e/playwright-report/ +e2e/.cache/ + +# misc +.DS_Store +.claude/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8ffb891 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 +# Imagen única: construye el front (Vite) y lo sirve desde el servidor (Fastify + Socket.IO). + +# ---- build del front ---- +FROM node:22-alpine AS web +WORKDIR /web +COPY web/package.json web/package-lock.json* ./ +RUN npm install +COPY web/ ./ +RUN npm run build # -> /web/dist + +# ---- producción ---- +FROM node:22-alpine AS production +WORKDIR /app +# deps del servidor (incluye tsx y drizzle-kit; NODE_ENV se fija DESPUÉS para no +# saltarse las devDependencies durante la instalación) +COPY server/package.json server/package-lock.json* ./ +RUN npm install +# código del servidor y front compilado +COPY server/ ./ +COPY --from=web /web/dist ./public +ENV NODE_ENV=production +ENV WEB_DIST=/app/public +ENV PORT=3001 +EXPOSE 3001 +# aplica el esquema (drizzle-kit push) y arranca el servidor +CMD ["sh", "-c", "npx drizzle-kit push --force && npx tsx src/index.ts"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..552b1af --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Sternboard + +> En náutica, un barco *makes a sternboard* cuando avanza hacia **atrás**, popa +> primero. Eso es una retro: mirar atrás para seguir adelante. _stern_ (atrás) + +> _board_ (tablero). + +Clon libre de retro board, sin registro. Tableros por columnas con plantillas, +revelación de notas a demanda, timer compartido, votos configurables, action +items y export a Markdown. Cada board puede tener un **temporizador de +autodestrucción** elegido al crearlo. + +## Arquitectura + +- **Servidor autoritativo** (Fastify + Socket.IO). Una room de Socket.IO por + board. El estado vive en Postgres (fuente de verdad) y se hidrata en memoria + por room; cada acción escribe en DB y hace broadcast del estado al resto. +- **Identidad sin login**: el board es un slug no adivinable en la URL. Al entrar + se pide nombre (obligatorio) y se genera un `participantId` (UUID) guardado en + `localStorage`. El creador recibe un `moderatorToken`. +- **Autodestrucción**: `boards.expires_at` se fija al crear; un job borra los + expirados y el front muestra la cuenta atrás. + +``` +web/ Vite + React + TS (UI del board) +server/ Fastify + Socket.IO + Drizzle (Postgres) +``` + +## Arrancar en local + +```bash +# 1. Levanta Postgres +docker compose up -d db + +# 2. Servidor +cd server +cp .env.example .env +npm install +npm run db:push # crea las tablas +npm run dev # http://localhost:3001 + +# 3. Front (otra terminal) +cd web +npm install +npm run dev # http://localhost:5173 +``` + +O todo en contenedores: `docker compose up --build`. + +## Protocolo Socket.IO + +Cliente → servidor: `board:join`, `note:add`, `note:update`, `note:delete`, +`note:move`, `vote:toggle`, `board:reveal`, `board:phase`, `timer:start`, +`timer:stop`, `actionitem:add`, `actionitem:update`, `actionitem:delete`. + +Servidor → cliente: `board:state` (estado completo), `error`. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cf0c03d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: retro + POSTGRES_PASSWORD: retro + POSTGRES_DB: retro + ports: + - "5433:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U retro"] + interval: 5s + timeout: 5s + retries: 5 + + app: + build: + context: . + dockerfile: Dockerfile + target: production + environment: + DATABASE_URL: postgres://retro:retro@db:5432/retro + PORT: "3001" + WEB_ORIGIN: "*" + ports: + - "3001:3001" + depends_on: + db: + condition: service_healthy + +volumes: + pgdata: diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 0000000..e0dcadf --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,61 @@ +import postgres from "postgres"; + +// Crea la base de datos e2e y su esquema desde cero antes de arrancar los servidores. +const DDL = ` +DROP TABLE IF EXISTS action_items, votes, notes, groups, columns, boards CASCADE; +CREATE TABLE boards ( + id text PRIMARY KEY, title text NOT NULL, phase text NOT NULL DEFAULT 'writing', + revealed boolean NOT NULL DEFAULT false, votes_revealed boolean NOT NULL DEFAULT false, + votes_per_user integer NOT NULL DEFAULT 3, allow_multi_vote boolean NOT NULL DEFAULT true, + moderation_mode text NOT NULL DEFAULT 'everyone', show_card_creators boolean NOT NULL DEFAULT true, + locked boolean NOT NULL DEFAULT false, + timer_ends_at timestamptz, timer_running boolean NOT NULL DEFAULT false, + moderator_token text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz +); +CREATE TABLE columns ( + id text PRIMARY KEY, board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + title text NOT NULL, position integer NOT NULL +); +CREATE TABLE groups ( + id text PRIMARY KEY, board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + column_id text NOT NULL REFERENCES columns(id) ON DELETE CASCADE, + title text NOT NULL DEFAULT '', position double precision NOT NULL DEFAULT 0 +); +CREATE TABLE notes ( + id text PRIMARY KEY, board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + column_id text NOT NULL REFERENCES columns(id) ON DELETE CASCADE, + group_id text REFERENCES groups(id) ON DELETE SET NULL, + author_id text NOT NULL, author_name text NOT NULL, text text NOT NULL, color text, + revealed boolean NOT NULL DEFAULT false, position double precision NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE TABLE votes ( + board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + target_type text NOT NULL, target_id text NOT NULL, participant_id text NOT NULL, + count integer NOT NULL DEFAULT 1 +); +CREATE UNIQUE INDEX votes_target_participant_uniq ON votes (target_id, participant_id); +CREATE TABLE action_items ( + id text PRIMARY KEY, board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + text text NOT NULL, owner text, done boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now() +); +`; + +export default async function globalSetup() { + const url = process.env.E2E_DATABASE_URL ?? "postgres://retro:retro@localhost:5433/retro_e2e"; + const dbName = new URL(url).pathname.slice(1); + const admin = postgres(url.replace(`/${dbName}`, "/postgres"), { max: 1 }); + try { + const exists = await admin`SELECT 1 FROM pg_database WHERE datname = ${dbName}`; + if (exists.length === 0) await admin.unsafe(`CREATE DATABASE ${dbName}`); + } finally { + await admin.end(); + } + const sql = postgres(url, { max: 1 }); + try { + await sql.unsafe(DDL); + } finally { + await sql.end(); + } +} diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..4232632 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "retropentool-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "retropentool-e2e", + "devDependencies": { + "@playwright/test": "^1.49.1", + "postgres": "^3.4.5" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postgres": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..c51bcc1 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,13 @@ +{ + "name": "sternboard-e2e", + "private": true, + "type": "module", + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed" + }, + "devDependencies": { + "@playwright/test": "^1.49.1", + "postgres": "^3.4.5" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..ac1aa43 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test"; +import { fileURLToPath } from "node:url"; + +const E2E_DB = process.env.E2E_DATABASE_URL ?? "postgres://retro:retro@localhost:5433/retro_e2e"; +const serverDir = fileURLToPath(new URL("../server", import.meta.url)); +const webDir = fileURLToPath(new URL("../web", import.meta.url)); + +export default defineConfig({ + testDir: "./tests", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + workers: 1, + globalSetup: "./global-setup.ts", + reporter: [["list"]], + use: { + baseURL: "http://localhost:5173", + locale: "en-US", // i18n -> inglés determinista + trace: "on-first-retry", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: [ + { + command: "npm run serve", + cwd: serverDir, + port: 3001, + reuseExistingServer: !process.env.CI, + timeout: 60_000, + env: { + DATABASE_URL: E2E_DB, + PORT: "3001", + WEB_ORIGIN: "http://localhost:5173", + }, + }, + { + command: "npm run dev", + cwd: webDir, + port: 5173, + reuseExistingServer: !process.env.CI, + timeout: 60_000, + }, + ], +}); diff --git a/e2e/tests/columns.spec.ts b/e2e/tests/columns.spec.ts new file mode 100644 index 0000000..deaa8b0 --- /dev/null +++ b/e2e/tests/columns.spec.ts @@ -0,0 +1,40 @@ +import { test, expect, type Page } from "@playwright/test"; + +async function createBoard(page: Page, title: string) { + await page.goto("/"); + await page.getByTestId("board-title").fill(title); + await page.getByTestId("template").selectOption("mad-sad-glad"); + await page.getByTestId("create").click(); + await page.waitForURL(/\/b\/.+/); +} +async function joinAs(page: Page, name: string) { + await page.getByTestId("name").fill(name); + await page.getByTestId("join").click(); + await expect(page.getByTestId("column").first()).toBeVisible(); +} + +test("añadir y eliminar columnas", async ({ page }) => { + await createBoard(page, "Retro columnas"); + await joinAs(page, "Ana"); + await expect(page.getByTestId("column")).toHaveCount(3); + + await page.getByTestId("add-column").click(); + await expect(page.getByTestId("column")).toHaveCount(4); + + // eliminar la última vía menú ⋯ + await page.getByTestId("column").last().getByTestId("col-menu").click(); + await page.getByTestId("col-delete").click(); + await expect(page.getByTestId("column")).toHaveCount(3); +}); + +test("fases Lluvia/Votación/Discusión", async ({ page }) => { + await createBoard(page, "Retro fases"); + await joinAs(page, "Ana"); + + // por defecto Lluvia (writing) activa + await expect(page.getByTestId("phase-writing")).toHaveClass(/on/); + + await page.getByTestId("phase-voting").click(); + await expect(page.getByTestId("phase-voting")).toHaveClass(/on/); + await expect(page.getByTestId("phase-writing")).not.toHaveClass(/on/); +}); diff --git a/e2e/tests/groups.spec.ts b/e2e/tests/groups.spec.ts new file mode 100644 index 0000000..5ff1812 --- /dev/null +++ b/e2e/tests/groups.spec.ts @@ -0,0 +1,99 @@ +import { test, expect, type Page, type Locator } from "@playwright/test"; + +async function createBoard(page: Page, title: string) { + await page.goto("/"); + await page.getByTestId("board-title").fill(title); + await page.getByTestId("template").selectOption("mad-sad-glad"); + await page.getByTestId("create").click(); + await page.waitForURL(/\/b\/.+/); + return page.url(); +} + +async function joinAs(page: Page, boardUrl: string, name: string) { + await page.goto(boardUrl); + await page.getByTestId("name").fill(name); + await page.getByTestId("join").click(); + await expect(page.getByTestId("column").first()).toBeVisible(); +} + +// Escribe una nota privada y la publica (queda en el área pública). +// Esperamos el conteo de notas privadas para evitar clics sobre elementos que re-renderizan. +async function addPublicNote(col: Locator, text: string) { + await col.getByTestId("note-input").fill(text); + await col.getByTestId("note-input").press("Enter"); + await expect(col.getByTestId("private-note")).toHaveCount(1); + await col.getByTestId("private-note").getByTestId("publish").click(); + await expect(col.getByTestId("private-note")).toHaveCount(0); +} + +// Arrastra una nota (desde su tirador) sobre el centro de otra (gesto de agrupar). +async function dragOnto(page: Page, sourceNote: Locator, target: Locator) { + const handle = await sourceNote.getByTestId("note-handle").boundingBox(); + const t = await target.boundingBox(); + if (!handle || !t) throw new Error("sin boundingBox"); + await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2); + await page.mouse.down(); + await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2 + 8); // supera el umbral + await page.mouse.move(t.x + t.width / 2, t.y + t.height / 2, { steps: 12 }); + await page.mouse.up(); +} + +test("agrupar dos notas arrastrando una sobre otra", async ({ page }) => { + const url = await createBoard(page, "Retro grupos"); + await joinAs(page, url, "Ana"); + + const col = page.getByTestId("column").first(); + await addPublicNote(col, "primera"); + await addPublicNote(col, "segunda"); + await expect(col.getByTestId("note")).toHaveCount(2); + + const notes = col.getByTestId("note"); + await dragOnto(page, notes.nth(1), notes.nth(0)); + + // aparece un grupo con dos notas dentro + await expect(page.getByTestId("group")).toHaveCount(1); + await expect(page.getByTestId("group").getByTestId("note")).toHaveCount(2); + + // votar el grupo como unidad + await page.getByTestId("group").getByTestId("vote").click(); + await expect(page.getByTestId("group").getByTestId("vote")).toHaveClass(/voted/); + + // y nombrarlo + await page.getByTestId("group-title").fill("Tema común"); + await page.getByTestId("group-title").blur(); + await expect(page.getByTestId("group-title")).toHaveValue("Tema común"); +}); + +test("multivoto: apilar varios votos en el mismo ítem y quitar", async ({ page }) => { + const url = await createBoard(page, "Retro multivoto"); + await joinAs(page, url, "Ana"); + + const col = page.getByTestId("column").first(); + await addPublicNote(col, "favorita"); + await expect(col.getByTestId("note")).toHaveCount(1); + + // dos votos al mismo ítem (multivoto activo por defecto) + await page.getByTestId("vote").click(); + await page.getByTestId("vote").click(); + + // el moderador revela los totales -> debe verse 2 + await page.getByTestId("reveal-votes").click(); + await expect(page.getByTestId("vote")).toContainText("2"); + + // quitar uno -> 1 + await page.getByTestId("vote-remove").click(); + await expect(page.getByTestId("vote")).toContainText("1"); +}); + +test("el moderador cambia los ajustes en caliente (título y votos)", async ({ page }) => { + const url = await createBoard(page, "Original"); + await joinAs(page, url, "Mod"); + + await page.getByTestId("settings").click(); + await page.getByTestId("settings-title").fill("Renombrada en vivo"); + await page.getByTestId("settings-votes").fill("9"); + await page.getByTestId("settings-save").click(); + + await expect(page.locator("header h1")).toHaveText("Renombrada en vivo"); + await expect(page.locator(".votes-pill")).toContainText("9"); +}); diff --git a/e2e/tests/import-export.spec.ts b/e2e/tests/import-export.spec.ts new file mode 100644 index 0000000..c7ba3e9 --- /dev/null +++ b/e2e/tests/import-export.spec.ts @@ -0,0 +1,52 @@ +import { test, expect, type Page } from "@playwright/test"; + +async function createBoard(page: Page, title: string) { + await page.goto("/"); + await page.getByTestId("board-title").fill(title); + await page.getByTestId("template").selectOption("mad-sad-glad"); + await page.getByTestId("create").click(); + await page.waitForURL(/\/b\/.+/); + return page.url(); +} + +async function joinAs(page: Page, boardUrl: string, name: string) { + await page.goto(boardUrl); + await page.getByTestId("name").fill(name); + await page.getByTestId("join").click(); + await expect(page.getByTestId("column").first()).toBeVisible(); +} + +test("exportar a JSON e importar: estructura vs todo", async ({ page, request }) => { + const url = await createBoard(page, "Fuente"); + const boardId = url.split("/b/")[1]; + await joinAs(page, url, "Origen"); + + const col = page.getByTestId("column").first(); + await col.getByTestId("note-input").fill("nota a exportar"); + await col.getByTestId("note-input").press("Enter"); + await col.getByTestId("publish").first().click(); // publicada para que viaje visible + await expect(col.getByTestId("note")).toHaveCount(1); + + const snapshotJson = await ( + await request.get(`http://localhost:3001/api/boards/${boardId}/export.json`) + ).text(); + const filePayload = { name: "fuente.retro.json", mimeType: "application/json", buffer: Buffer.from(snapshotJson) }; + + // --- import solo estructura: 3 columnas, 0 notas --- + await page.goto("/"); + await page.getByTestId("import-toggle").click(); + await page.getByText("Only structure", { exact: false }).click(); + await page.getByTestId("import-file").setInputFiles(filePayload); + await page.waitForURL(/\/b\/.+/); + await joinAs(page, page.url(), "Importador"); + await expect(page.getByTestId("column")).toHaveCount(3); + await expect(page.getByTestId("note")).toHaveCount(0); + + // --- import completo: la nota viaja --- + await page.goto("/"); + await page.getByTestId("import-toggle").click(); + await page.getByTestId("import-file").setInputFiles(filePayload); // modo 'full' por defecto + await page.waitForURL(/\/b\/.+/); + await joinAs(page, page.url(), "Importador2"); + await expect(page.getByTestId("note")).toHaveCount(1); +}); diff --git a/e2e/tests/panels.spec.ts b/e2e/tests/panels.spec.ts new file mode 100644 index 0000000..af70bdc --- /dev/null +++ b/e2e/tests/panels.spec.ts @@ -0,0 +1,92 @@ +import { test, expect, type Page, type Locator } from "@playwright/test"; + +async function createBoard(page: Page, title: string) { + await page.goto("/"); + await page.getByTestId("board-title").fill(title); + await page.getByTestId("template").selectOption("mad-sad-glad"); + await page.getByTestId("create").click(); + await page.waitForURL(/\/b\/.+/); + return page.url(); +} +async function joinAs(page: Page, url: string, name: string) { + await page.goto(url); + await page.getByTestId("name").fill(name); + await page.getByTestId("join").click(); + await expect(page.getByTestId("column").first()).toBeVisible(); +} +async function addPublicNote(col: Locator, text: string) { + await col.getByTestId("note-input").fill(text); + await col.getByTestId("note-input").press("Enter"); + await expect(col.getByTestId("private-note")).toHaveCount(1); + await col.getByTestId("private-note").getByTestId("publish").click(); + await expect(col.getByTestId("private-note")).toHaveCount(0); +} + +test("participantes: presencia, editar nombre y mostrar autores", async ({ page }) => { + const url = await createBoard(page, "Retro panel"); + await joinAs(page, url, "Ana"); + const col = page.getByTestId("column").first(); + await addPublicNote(col, "hola"); + await expect(col.locator(".note .author")).toHaveText("Ana"); + + await page.getByTestId("open-participants").click(); + const people = page.locator(".people"); + await expect(people).toContainText("Ana"); + + // editar mi nombre + await page.getByTestId("edit-name").click(); + await page.getByTestId("name-input").fill("Anita"); + await page.getByTestId("name-save").click(); + await expect(people).toContainText("Anita"); + + // ocultar autores de las tarjetas (checkbox controlado por el servidor -> click) + await page.getByTestId("show-creators").click(); + await page.mouse.click(20, 400); // cerrar el drawer pulsando en el backdrop + await expect(col.locator(".note .author")).toHaveCount(0); +}); + +test("action points: añadir y mover a Done", async ({ page }) => { + const url = await createBoard(page, "Retro acciones"); + await joinAs(page, url, "Ana"); + + await page.getByTestId("open-actions").click(); + await page.getByTestId("action-input").fill("documentar el deploy"); + await page.getByTestId("action-add").click(); + + await expect(page.getByTestId("todo-list").getByTestId("action-row")).toHaveCount(1); + + // marcar como hecho -> la fila se mueve a Done (la casilla se desmonta del To-do) + await page.getByTestId("todo-list").getByTestId("action-done").first().click(); + await expect(page.getByTestId("todo-list").getByTestId("action-row")).toHaveCount(0); + await expect(page.getByTestId("done-list").getByTestId("action-row")).toHaveCount(1); +}); + +test("export modal: previsualización con opciones", async ({ page }) => { + const url = await createBoard(page, "Retro export"); + await joinAs(page, url, "Ana"); + const col = page.getByTestId("column").first(); + await addPublicNote(col, "algo"); + + await page.getByTestId("settings").click(); + await page.getByTestId("open-export").click(); + const preview = page.getByTestId("export-preview"); + await expect(preview).toContainText("Retro export"); + await expect(preview).toContainText("algo"); + + // desactivar autores -> desaparece el nombre del preview + await expect(preview).toContainText("_Ana_"); + await page.getByTestId("opt-authors").uncheck(); + await expect(preview).not.toContainText("_Ana_"); +}); + +test("opciones: bloquear deja el board en solo lectura", async ({ page }) => { + const url = await createBoard(page, "Retro lock"); + await joinAs(page, url, "Ana"); + + await page.getByTestId("settings").click(); + await page.getByTestId("opt-lock").click(); + await page.mouse.click(20, 400); // cerrar el panel + + await expect(page.locator(".locked-banner")).toBeVisible(); + await expect(page.getByTestId("private-section")).toHaveCount(0); // sin sección privada al bloquear +}); diff --git a/e2e/tests/retro.spec.ts b/e2e/tests/retro.spec.ts new file mode 100644 index 0000000..3891076 --- /dev/null +++ b/e2e/tests/retro.spec.ts @@ -0,0 +1,123 @@ +import { test, expect, type Page, type Locator } from "@playwright/test"; + +async function createBoard(page: Page, title: string, template = "mad-sad-glad") { + await page.goto("/"); + await page.getByTestId("board-title").fill(title); + await page.getByTestId("template").selectOption(template); + await page.getByTestId("create").click(); + await page.waitForURL(/\/b\/.+/); + return page.url(); +} + +async function joinAs(page: Page, boardUrl: string, name: string) { + await page.goto(boardUrl); + await expect(page.getByTestId("name")).toBeVisible(); + await page.getByTestId("name").fill(name); + await page.getByTestId("join").click(); + await expect(page.getByTestId("column").first()).toBeVisible(); +} + +// Escribe una nota en la sección privada de una columna (se guarda con Enter). +async function writeNote(col: Locator, text: string) { + await col.getByTestId("note-input").fill(text); + await col.getByTestId("note-input").press("Enter"); + await expect(col.getByTestId("private-note").last()).toBeVisible(); +} + +test("crea un board, pide nombre obligatorio y muestra columnas", async ({ page }) => { + await createBoard(page, "Sprint 42"); + await expect(page.getByTestId("name")).toBeVisible(); + await expect(page.getByTestId("column")).toHaveCount(0); + + await page.getByTestId("name").fill("Fede"); + await page.getByTestId("join").click(); + + await expect(page.getByTestId("column")).toHaveCount(3); +}); + +test("una nota privada no la ve nadie hasta publicarla", async ({ browser }) => { + const ctxA = await browser.newContext(); + const ctxB = await browser.newContext(); + const alice = await ctxA.newPage(); + const bob = await ctxB.newPage(); + + const url = await createBoard(alice, "Retro privada"); + await joinAs(alice, url, "Alice"); + await joinAs(bob, url, "Bob"); + + const aliceCol = alice.getByTestId("column").first(); + await writeNote(aliceCol, "algo privado"); + + // Bob no ve nada: ni nota pública ni la privada de Alice + await expect(bob.getByTestId("note")).toHaveCount(0); + await expect(bob.getByTestId("private-note")).toHaveCount(0); + + // Alice publica su nota + await aliceCol.getByTestId("publish").first().click(); + + // ahora es pública para Bob (la ve como párrafo de solo lectura) + await expect(bob.getByTestId("note")).toHaveCount(1); + await expect(bob.getByTestId("note-text")).toHaveText("algo privado"); + + await ctxA.close(); + await ctxB.close(); +}); + +test("Publish all publica todas mis notas de la columna", async ({ page }) => { + const url = await createBoard(page, "Retro publish all"); + await joinAs(page, url, "Vera"); + + const col = page.getByTestId("column").first(); + await writeNote(col, "una"); + await writeNote(col, "dos"); + await expect(col.getByTestId("private-note")).toHaveCount(2); + + await col.getByTestId("publish-all").click(); + + await expect(col.getByTestId("private-note")).toHaveCount(0); + await expect(col.getByTestId("note")).toHaveCount(2); +}); + +test("votar una nota publicada y exportar markdown con el conteo", async ({ page, request }) => { + const url = await createBoard(page, "Retro votos"); + const boardId = url.split("/b/")[1]; + await joinAs(page, url, "Vera"); + + const col = page.getByTestId("column").first(); + await writeNote(col, "idea top"); + await col.getByTestId("publish").first().click(); + await expect(page.getByTestId("note")).toHaveCount(1); + + // votar: con totales ocultos el botón marca "✓" (mi voto), no el total + await page.getByTestId("vote").first().click(); + await expect(page.getByTestId("vote").first()).toHaveClass(/voted/); + + // el moderador revela los votos -> ahora se ve el total + await page.getByTestId("reveal-votes").click(); + await expect(page.getByTestId("vote").first()).toContainText("1"); + + const res = await request.get(`http://localhost:3001/api/boards/${boardId}/export`); + expect(res.status()).toBe(200); + const md = await res.text(); + expect(md).toContain("# Retro votos"); + expect(md).toContain("idea top — _Vera_ (1 👍)"); +}); + +test("el timer compartido arranca para todos los participantes", async ({ browser }) => { + const ctxA = await browser.newContext(); + const ctxB = await browser.newContext(); + const alice = await ctxA.newPage(); + const bob = await ctxB.newPage(); + + const url = await createBoard(alice, "Retro timer"); + await joinAs(alice, url, "Alice"); + await joinAs(bob, url, "Bob"); + + await alice.getByRole("button", { name: /5\s*min/i }).click(); + + await expect(alice.locator(".timer .clock")).toHaveText(/0[0-4]:\d{2}/); + await expect(bob.locator(".timer .clock")).toHaveText(/0[0-4]:\d{2}/); + + await ctxA.close(); + await ctxB.close(); +}); diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..98ad116 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL=postgres://retro:retro@localhost:5433/retro +PORT=3001 +WEB_ORIGIN=http://localhost:5173 diff --git a/server/drizzle.config.ts b/server/drizzle.config.ts new file mode 100644 index 0000000..d3d75b8 --- /dev/null +++ b/server/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "drizzle-kit"; + +export default { + schema: "./src/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL ?? "postgres://retro:retro@localhost:5433/retro", + }, +} satisfies Config; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..8d156e9 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,4426 @@ +{ + "name": "sternboard-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sternboard-server", + "dependencies": { + "@fastify/cors": "^10.0.1", + "@fastify/static": "^8.3.0", + "drizzle-orm": "^0.36.4", + "fastify": "^5.1.0", + "nanoid": "^5.0.9", + "postgres": "^3.4.5", + "socket.io": "^4.8.1" + }, + "devDependencies": { + "drizzle-kit": "^0.28.1", + "socket.io-client": "^4.8.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/drizzle-kit": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.28.1.tgz", + "integrity": "sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.36.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.36.4.tgz", + "integrity": "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/engine.io": { + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz", + "integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.5.tgz", + "integrity": "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", + "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/postgres": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz", + "integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.20.1" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", + "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..c499035 --- /dev/null +++ b/server/package.json @@ -0,0 +1,29 @@ +{ + "name": "sternboard-server", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "serve": "tsx src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "db:push": "drizzle-kit push", + "test": "vitest run" + }, + "dependencies": { + "@fastify/cors": "^10.0.1", + "@fastify/static": "^8.3.0", + "drizzle-orm": "^0.36.4", + "fastify": "^5.1.0", + "nanoid": "^5.0.9", + "postgres": "^3.4.5", + "socket.io": "^4.8.1" + }, + "devDependencies": { + "drizzle-kit": "^0.28.1", + "socket.io-client": "^4.8.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + } +} diff --git a/server/src/app.ts b/server/src/app.ts new file mode 100644 index 0000000..fae44f0 --- /dev/null +++ b/server/src/app.ts @@ -0,0 +1,278 @@ +import Fastify, { type FastifyInstance } from "fastify"; +import cors from "@fastify/cors"; +import fastifyStatic from "@fastify/static"; +import { Server } from "socket.io"; +import { lt, isNotNull, and } from "drizzle-orm"; +import { nanoid } from "nanoid"; +import { db, schema } from "./db/index.js"; +import { TEMPLATES, getTemplate } from "./templates.js"; +import { registerSockets } from "./socket.js"; +import { loadBoardState } from "./room.js"; +import { boardToMarkdown, DEFAULT_MD_OPTIONS } from "./markdown.js"; + +export type BuiltServer = { + app: FastifyInstance; + io: Server; + listen: (port: number) => Promise; + close: () => Promise; +}; + +export function buildServer(opts: { webOrigin?: string; cleanup?: boolean } = {}): BuiltServer { + const webOrigin = opts.webOrigin ?? process.env.WEB_ORIGIN ?? "http://localhost:5173"; + const app = Fastify({ logger: false }); + let cleanupTimer: ReturnType | null = null; + + app.register(cors, { origin: webOrigin }); + + app.get("/api/templates", async () => TEMPLATES); + + app.post("/api/boards", async (req, reply) => { + const body = req.body as { + title?: string; + templateId?: string; + columns?: string[]; + votesPerUser?: number; + ttlHours?: number | null; + }; + const title = (body.title ?? "").trim() || "Retro"; + const votesPerUser = clampInt(body.votesPerUser ?? 3, 0, 50); + + let columnTitles: string[] = []; + if (body.templateId) { + const tpl = getTemplate(body.templateId); + if (!tpl) return reply.code(400).send({ error: "template_not_found" }); + columnTitles = tpl.columns; + } else if (body.columns && body.columns.length > 0) { + columnTitles = body.columns.map((c) => c.trim()).filter(Boolean); + } + if (columnTitles.length === 0) columnTitles = ["Columna 1", "Columna 2", "Columna 3"]; + + const id = nanoid(12); + const moderatorToken = nanoid(24); + const expiresAt = + body.ttlHours && body.ttlHours > 0 ? new Date(Date.now() + body.ttlHours * 3600 * 1000) : null; + + await db.transaction(async (tx) => { + await tx.insert(schema.boards).values({ id, title, votesPerUser, moderatorToken, expiresAt }); + await tx + .insert(schema.columns) + .values(columnTitles.map((t, i) => ({ id: nanoid(), boardId: id, title: t, position: i }))); + }); + + return reply.send({ boardId: id, moderatorToken, expiresAt: expiresAt?.toISOString() ?? null }); + }); + + app.get("/api/boards/:id/export", async (req, reply) => { + const { id } = req.params as { id: string }; + const state = await loadBoardState(id); + if (!state) return reply.code(404).send({ error: "board_not_found" }); + const q = req.query as Record; + // un parámetro presente y != '0'/'false' cuenta como activado; si no se pasa ninguno, defaults + const on = (v: string | undefined, dflt: boolean) => (v === undefined ? dflt : v !== "0" && v !== "false"); + const md = boardToMarkdown(state, { + authors: on(q.authors, DEFAULT_MD_OPTIONS.authors), + votes: on(q.votes, DEFAULT_MD_OPTIONS.votes), + expandGroups: on(q.groups, DEFAULT_MD_OPTIONS.expandGroups), + creationDate: on(q.created, DEFAULT_MD_OPTIONS.creationDate), + exportDate: on(q.exported, DEFAULT_MD_OPTIONS.exportDate), + }); + // ?download=1 fuerza descarga; si no, texto inline (para la previsualización) + if (q.download && q.download !== "0") { + reply.header("Content-Disposition", `attachment; filename="${slugify(state.title)}.md"`); + } + reply.header("Content-Type", "text/markdown; charset=utf-8").send(md); + }); + + app.get("/api/boards/:id", async (req, reply) => { + const { id } = req.params as { id: string }; + const state = await loadBoardState(id); + if (!state) return reply.code(404).send({ error: "board_not_found" }); + return reply.send({ id: state.id, title: state.title, expiresAt: state.expiresAt }); + }); + + // Export completo del board a JSON (config + columnas + grupos + notas + votos + acciones). + app.get("/api/boards/:id/export.json", async (req, reply) => { + const { id } = req.params as { id: string }; + const state = await loadBoardState(id); + if (!state) return reply.code(404).send({ error: "board_not_found" }); + const snapshot = { + version: 1 as const, + board: { title: state.title, votesPerUser: state.votesPerUser, allowMultiVote: state.allowMultiVote }, + columns: state.columns, + groups: state.groups, + notes: state.notes, + votes: state.votes, + actionItems: state.actionItems.map((a) => ({ text: a.text, owner: a.owner })), + }; + reply + .header("Content-Type", "application/json; charset=utf-8") + .header("Content-Disposition", `attachment; filename="${slugify(state.title)}.retro.json"`) + .send(JSON.stringify(snapshot, null, 2)); + }); + + // Import: crea un board NUEVO desde un snapshot. mode 'full' (todo) o 'structure' (solo columnas+config). + app.post("/api/boards/import", async (req, reply) => { + const body = req.body as { + snapshot?: ImportSnapshot; + mode?: "full" | "structure"; + title?: string; + ttlHours?: number | null; + }; + const snap = body.snapshot; + const mode = body.mode === "structure" ? "structure" : "full"; + if (!snap || !Array.isArray(snap.columns) || snap.columns.length === 0) { + return reply.code(400).send({ error: "invalid_snapshot" }); + } + const result = await importBoard(snap, mode, { title: body.title, ttlHours: body.ttlHours }); + return reply.send(result); + }); + + // En producción servimos el front estático (build de Vite) desde el mismo servidor, + // con fallback SPA: cualquier ruta que no sea /api ni /socket.io devuelve index.html. + if (process.env.WEB_DIST) { + app.register(fastifyStatic, { root: process.env.WEB_DIST, prefix: "/" }); + app.setNotFoundHandler((req, reply) => { + if (req.url.startsWith("/api") || req.url.startsWith("/socket.io")) { + return reply.code(404).send({ error: "not_found" }); + } + return reply.sendFile("index.html"); + }); + } + + const io = new Server(app.server, { cors: { origin: webOrigin } }); + registerSockets(io); + + if (opts.cleanup) { + cleanupTimer = setInterval(() => runCleanup(io).catch(() => {}), 60_000); + } + + return { + app, + io, + listen: async (port: number) => { + await app.listen({ port, host: "0.0.0.0" }); + const addr = app.server.address(); + return typeof addr === "object" && addr ? addr.port : port; + }, + close: async () => { + if (cleanupTimer) clearInterval(cleanupTimer); + io.close(); + await app.close(); + }, + }; +} + +// Borra boards caducados y avisa a sus rooms. Exportado para poder testearlo. +export async function runCleanup(io: Server): Promise { + const expired = await db + .delete(schema.boards) + .where(and(isNotNull(schema.boards.expiresAt), lt(schema.boards.expiresAt, new Date()))) + .returning({ id: schema.boards.id }); + for (const b of expired) io.to(b.id).emit("board:destroyed"); + return expired.map((b) => b.id); +} + +function clampInt(n: number, min: number, max: number) { + return Math.max(min, Math.min(max, Math.round(n))); +} +function slugify(s: string) { + return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "retro"; +} + +export type ImportSnapshot = { + version: number; + board: { title: string; votesPerUser: number; allowMultiVote?: boolean }; + columns: { id: string; title: string; position: number }[]; + groups: { id: string; columnId: string; title: string; position: number }[]; + notes: { + id: string; + columnId: string; + groupId: string | null; + authorId: string; + authorName: string; + text: string; + color?: string | null; + revealed: boolean; + position: number; + }[]; + votes: { targetType: string; targetId: string; participantId: string; count?: number }[]; + actionItems: { text: string; owner: string | null }[]; +}; + +// Crea un board nuevo desde un snapshot, remapeando todos los ids. +// 'structure' importa solo columnas + config; 'full' importa todo el contenido. +export async function importBoard( + snap: ImportSnapshot, + mode: "full" | "structure", + opts: { title?: string; ttlHours?: number | null }, +): Promise<{ boardId: string; moderatorToken: string; expiresAt: string | null }> { + const id = nanoid(12); + const moderatorToken = nanoid(24); + const title = (opts.title ?? snap.board?.title ?? "Retro").trim() || "Retro"; + const votesPerUser = clampInt(snap.board?.votesPerUser ?? 3, 0, 50); + const allowMultiVote = snap.board?.allowMultiVote ?? true; + const expiresAt = + opts.ttlHours && opts.ttlHours > 0 ? new Date(Date.now() + opts.ttlHours * 3600 * 1000) : null; + + await db.transaction(async (tx) => { + await tx.insert(schema.boards).values({ id, title, votesPerUser, allowMultiVote, moderatorToken, expiresAt }); + + const colMap = new Map(); + for (const c of snap.columns) { + const newId = nanoid(); + colMap.set(c.id, newId); + await tx.insert(schema.columns).values({ id: newId, boardId: id, title: c.title, position: c.position }); + } + if (mode === "structure") return; + + const groupMap = new Map(); + for (const g of snap.groups ?? []) { + const colId = colMap.get(g.columnId); + if (!colId) continue; + const newId = nanoid(); + groupMap.set(g.id, newId); + await tx + .insert(schema.groups) + .values({ id: newId, boardId: id, columnId: colId, title: g.title, position: g.position }); + } + + const targetMap = new Map(); // viejo id (nota/grupo) -> nuevo id, para los votos + for (const [oldG, newG] of groupMap) targetMap.set(oldG, newG); + for (const n of snap.notes ?? []) { + const colId = colMap.get(n.columnId); + if (!colId) continue; + const newId = nanoid(); + targetMap.set(n.id, newId); + await tx.insert(schema.notes).values({ + id: newId, + boardId: id, + columnId: colId, + groupId: n.groupId ? groupMap.get(n.groupId) ?? null : null, + authorId: n.authorId, + authorName: n.authorName, + text: n.text, + color: n.color ?? null, + revealed: n.revealed, + position: n.position, + }); + } + + for (const v of snap.votes ?? []) { + const targetId = targetMap.get(v.targetId); + if (!targetId) continue; + await tx.insert(schema.votes).values({ + boardId: id, + targetType: v.targetType, + targetId, + participantId: v.participantId, + count: v.count ?? 1, + }); + } + + for (const a of snap.actionItems ?? []) { + await tx.insert(schema.actionItems).values({ id: nanoid(), boardId: id, text: a.text, owner: a.owner }); + } + }); + + return { boardId: id, moderatorToken, expiresAt: expiresAt?.toISOString() ?? null }; +} diff --git a/server/src/db/index.ts b/server/src/db/index.ts new file mode 100644 index 0000000..46743dd --- /dev/null +++ b/server/src/db/index.ts @@ -0,0 +1,12 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema.js"; + +const url = process.env.DATABASE_URL; +if (!url) throw new Error("DATABASE_URL no definido"); + +// max alto: las transacciones de voto usan FOR UPDATE y, bajo ráfagas concurrentes, +// no deben quedarse sin conexión (evita timeouts esporádicos). +const client = postgres(url, { max: 20 }); +export const db = drizzle(client, { schema }); +export { schema }; diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts new file mode 100644 index 0000000..b60506c --- /dev/null +++ b/server/src/db/schema.ts @@ -0,0 +1,95 @@ +import { + pgTable, + text, + integer, + boolean, + timestamp, + doublePrecision, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +export const boards = pgTable("boards", { + id: text("id").primaryKey(), // slug no adivinable + title: text("title").notNull(), + phase: text("phase").notNull().default("writing"), // writing | revealed | voting | done + revealed: boolean("revealed").notNull().default(false), // moderador revela TODAS las notas + votesRevealed: boolean("votes_revealed").notNull().default(false), // moderador revela los totales + votesPerUser: integer("votes_per_user").notNull().default(3), + allowMultiVote: boolean("allow_multi_vote").notNull().default(true), // varios votos al mismo ítem + moderationMode: text("moderation_mode").notNull().default("everyone"), // 'everyone' | 'single' + showCardCreators: boolean("show_card_creators").notNull().default(true), + locked: boolean("locked").notNull().default(false), // solo lectura + timerEndsAt: timestamp("timer_ends_at", { withTimezone: true }), + timerRunning: boolean("timer_running").notNull().default(false), + moderatorToken: text("moderator_token").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + expiresAt: timestamp("expires_at", { withTimezone: true }), // autodestrucción; null = sin caducidad +}); + +export const columns = pgTable("columns", { + id: text("id").primaryKey(), + boardId: text("board_id") + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + title: text("title").notNull(), + position: integer("position").notNull(), +}); + +// Un grupo (cluster) de notas dentro de una columna. Es la unidad votable. +export const groups = pgTable("groups", { + id: text("id").primaryKey(), + boardId: text("board_id") + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + columnId: text("column_id") + .notNull() + .references(() => columns.id, { onDelete: "cascade" }), + title: text("title").notNull().default(""), + position: doublePrecision("position").notNull().default(0), // fractional indexing +}); + +export const notes = pgTable("notes", { + id: text("id").primaryKey(), + boardId: text("board_id") + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + columnId: text("column_id") + .notNull() + .references(() => columns.id, { onDelete: "cascade" }), + groupId: text("group_id").references(() => groups.id, { onDelete: "set null" }), // null = suelta + authorId: text("author_id").notNull(), + authorName: text("author_name").notNull(), + text: text("text").notNull(), + color: text("color"), // color del post-it (hex); null = color por defecto + revealed: boolean("revealed").notNull().default(false), // el autor revela su propia nota + position: doublePrecision("position").notNull().default(0), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); + +// Voto sobre un votable genérico: una nota suelta o un grupo. +export const votes = pgTable( + "votes", + { + boardId: text("board_id") + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + targetType: text("target_type").notNull(), // 'note' | 'group' + targetId: text("target_id").notNull(), + participantId: text("participant_id").notNull(), + count: integer("count").notNull().default(1), // nº de votos de este participante a este ítem + }, + (t) => ({ + uniq: uniqueIndex("votes_target_participant_uniq").on(t.targetId, t.participantId), + }), +); + +export const actionItems = pgTable("action_items", { + id: text("id").primaryKey(), + boardId: text("board_id") + .notNull() + .references(() => boards.id, { onDelete: "cascade" }), + text: text("text").notNull(), + owner: text("owner"), + done: boolean("done").notNull().default(false), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/server/src/env.ts b/server/src/env.ts new file mode 100644 index 0000000..0695822 --- /dev/null +++ b/server/src/env.ts @@ -0,0 +1,10 @@ +// Carga .env (cargador nativo de Node) salvo que las vars ya vengan del entorno +// — así Playwright/CI pueden inyectar otra DATABASE_URL sin que el fichero la pise. +// Importar este módulo el PRIMERO de todos por su efecto secundario. +if (!process.env.DATABASE_URL) { + try { + process.loadEnvFile(); + } catch { + // sin .env: se usarán las variables del entorno + } +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..3e7d7d6 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,7 @@ +import "./env.js"; // primero: carga .env antes de tocar la DB +import { buildServer } from "./app.js"; + +const PORT = Number(process.env.PORT ?? 3001); +const server = buildServer({ cleanup: true }); +await server.listen(PORT); +console.log(`Sternboard server escuchando en :${PORT}`); diff --git a/server/src/markdown.ts b/server/src/markdown.ts new file mode 100644 index 0000000..7adbf92 --- /dev/null +++ b/server/src/markdown.ts @@ -0,0 +1,85 @@ +import type { BoardState } from "./room.js"; + +export type MarkdownOptions = { + authors: boolean; // mostrar nombres de autor + votes: boolean; // mostrar conteo de votos + expandGroups: boolean; // listar las notas dentro de cada grupo + creationDate: boolean; // añadir la fecha de creación al título + exportDate: boolean; // añadir línea con la fecha de exportación +}; + +export const DEFAULT_MD_OPTIONS: MarkdownOptions = { + authors: true, + votes: true, + expandGroups: true, + creationDate: false, + exportDate: false, +}; + +function fmtDate(d: Date): string { + return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; +} + +// Export del estado del board a Markdown, con opciones (grupos, autores, votos, fechas). +export function boardToMarkdown( + state: BoardState, + opts: MarkdownOptions = DEFAULT_MD_OPTIONS, + now: Date = new Date(), +): string { + const lines: string[] = []; + const created = opts.creationDate ? `${fmtDate(new Date(state.createdAt))} - ` : ""; + lines.push(`# ${created}${state.title}`); + if (opts.exportDate) lines.push(`_Exported: ${fmtDate(now)}_`); + lines.push(""); + + const votesOf = (targetId: string) => + state.votes.filter((v) => v.targetId === targetId).reduce((sum, v) => sum + v.count, 0); + const votePart = (n: number) => (opts.votes && n > 0 ? ` (${n} 👍)` : ""); + const authorPart = (name: string) => (opts.authors ? ` — _${name}_` : ""); + + for (const col of state.columns) { + lines.push(`## ${col.title}`); + + const groups = state.groups.filter((g) => g.columnId === col.id); + const standalone = state.notes.filter((n) => n.columnId === col.id && !n.groupId); + + type Item = { votes: number; render: () => string[] }; + const items: Item[] = []; + + for (const g of groups) { + const members = state.notes.filter((n) => n.groupId === g.id); + const v = votesOf(g.id); + items.push({ + votes: v, + render: () => { + const out = [`- **${g.title?.trim() || "Grupo"}**${votePart(v)}`]; + if (opts.expandGroups) for (const m of members) out.push(` - ${m.text}${authorPart(m.authorName)}`); + return out; + }, + }); + } + for (const n of standalone) { + const v = votesOf(n.id); + items.push({ votes: v, render: () => [`- ${n.text}${authorPart(n.authorName)}${votePart(v)}`] }); + } + + if (items.length === 0) { + lines.push("_(sin notas)_"); + } else { + items.sort((a, b) => b.votes - a.votes); + for (const it of items) lines.push(...it.render()); + } + lines.push(""); + } + + if (state.actionItems.length > 0) { + lines.push("## 📋 Action items"); + for (const a of state.actionItems) { + const owner = a.owner ? ` — **${a.owner}**` : ""; + lines.push(`- [${a.done ? "x" : " "}] ${a.text}${owner}`); + } + lines.push(""); + } + + return lines.join("\n"); +} diff --git a/server/src/room.ts b/server/src/room.ts new file mode 100644 index 0000000..a143056 --- /dev/null +++ b/server/src/room.ts @@ -0,0 +1,102 @@ +import { eq } from "drizzle-orm"; +import { db, schema } from "./db/index.js"; + +export type BoardState = { + id: string; + title: string; + phase: string; + revealed: boolean; // todas las notas reveladas (moderador) + votesRevealed: boolean; // totales de votos visibles (moderador) + votesPerUser: number; + allowMultiVote: boolean; + moderationMode: string; // 'everyone' | 'single' + showCardCreators: boolean; + locked: boolean; + createdAt: string; + timerEndsAt: string | null; + timerRunning: boolean; + expiresAt: string | null; + youAreModerator?: boolean; // se rellena por-viewer al difundir + participants?: { id: string; name: string }[]; // presencia, se rellena al difundir + columns: { id: string; title: string; position: number }[]; + groups: { id: string; columnId: string; title: string; position: number }[]; + notes: { + id: string; + columnId: string; + groupId: string | null; + authorId: string; + authorName: string; + text: string; + color: string | null; + revealed: boolean; + position: number; + }[]; + votes: { targetType: string; targetId: string; participantId: string; count: number }[]; + actionItems: { id: string; text: string; owner: string | null; done: boolean }[]; +}; + +export async function loadBoardState(boardId: string): Promise { + const [board] = await db.select().from(schema.boards).where(eq(schema.boards.id, boardId)); + if (!board) return null; + + const [cols, grps, ns, vs, ais] = await Promise.all([ + db.select().from(schema.columns).where(eq(schema.columns.boardId, boardId)), + db.select().from(schema.groups).where(eq(schema.groups.boardId, boardId)), + db.select().from(schema.notes).where(eq(schema.notes.boardId, boardId)), + db.select().from(schema.votes).where(eq(schema.votes.boardId, boardId)), + db.select().from(schema.actionItems).where(eq(schema.actionItems.boardId, boardId)), + ]); + + return { + id: board.id, + title: board.title, + phase: board.phase, + revealed: board.revealed, + votesRevealed: board.votesRevealed, + votesPerUser: board.votesPerUser, + allowMultiVote: board.allowMultiVote, + moderationMode: board.moderationMode, + showCardCreators: board.showCardCreators, + locked: board.locked, + createdAt: board.createdAt.toISOString(), + timerEndsAt: board.timerEndsAt ? board.timerEndsAt.toISOString() : null, + timerRunning: board.timerRunning, + expiresAt: board.expiresAt ? board.expiresAt.toISOString() : null, + columns: cols + .map((c) => ({ id: c.id, title: c.title, position: c.position })) + .sort((a, b) => a.position - b.position), + groups: grps.map((g) => ({ id: g.id, columnId: g.columnId, title: g.title, position: g.position })), + notes: ns.map((n) => ({ + id: n.id, + columnId: n.columnId, + groupId: n.groupId, + authorId: n.authorId, + authorName: n.authorName, + text: n.text, + color: n.color, + revealed: n.revealed, + position: n.position, + })), + votes: vs.map((v) => ({ + targetType: v.targetType, + targetId: v.targetId, + participantId: v.participantId, + count: v.count, + })), + actionItems: ais.map((a) => ({ id: a.id, text: a.text, owner: a.owner, done: a.done })), + }; +} + +// Vista que recibe un participante concreto: +// - texto de notas ajenas oculto salvo que el board o la nota estén revelados. +// - votos: si los totales no están revelados, solo ve los suyos. +export function viewFor(state: BoardState, participantId: string): BoardState { + const notes = state.notes.map((n) => { + const visible = state.revealed || n.revealed || !!n.groupId || n.authorId === participantId; + return visible ? n : { ...n, text: "" }; + }); + const votes = state.votesRevealed + ? state.votes + : state.votes.filter((v) => v.participantId === participantId); + return { ...state, notes, votes }; +} diff --git a/server/src/socket.ts b/server/src/socket.ts new file mode 100644 index 0000000..9880705 --- /dev/null +++ b/server/src/socket.ts @@ -0,0 +1,591 @@ +import type { Server } from "socket.io"; +import { and, eq, sql } from "drizzle-orm"; +import { nanoid } from "nanoid"; +import { db, schema } from "./db/index.js"; +import { loadBoardState, viewFor } from "./room.js"; + +type SessionData = { boardId: string; participantId: string; name: string; moderatorToken?: string }; + +// La sesión se guarda por socket.id para poder leerla también al difundir +// (fetchSockets devuelve sockets remotos sin acceso a closures). +const sessions = new Map(); + +export function registerSockets(io: Server) { + io.on("connection", (socket) => { + socket.on("disconnect", () => sessions.delete(socket.id)); + + socket.on( + "board:join", + async (p: { boardId: string; participantId: string; name: string; moderatorToken?: string }) => { + const state = await loadBoardState(p.boardId); + if (!state) return socket.emit("error", { message: "board_not_found" }); + if (!p.name?.trim()) return socket.emit("error", { message: "name_required" }); + sessions.set(socket.id, { + boardId: p.boardId, + participantId: p.participantId, + name: p.name.trim(), + moderatorToken: p.moderatorToken, + }); + socket.join(p.boardId); + await broadcast(io, p.boardId); + }, + ); + + const guard = (): SessionData | null => { + const s = sessions.get(socket.id); + if (!s) { + socket.emit("error", { message: "not_joined" }); + return null; + } + return s; + }; + + // moderador si el board es abierto ('everyone') o el token coincide ('single') + const requireMod = async (boardId: string, token?: string) => { + const [board] = await db.select().from(schema.boards).where(eq(schema.boards.id, boardId)); + if (!board) return false; + return board.moderationMode === "everyone" || board.moderatorToken === token; + }; + + // ¿el board está bloqueado (solo lectura)? bloquea las mutaciones de contenido. + const isLocked = async (boardId: string) => { + const [board] = await db + .select({ locked: schema.boards.locked }) + .from(schema.boards) + .where(eq(schema.boards.id, boardId)); + return !!board?.locked; + }; + // guard combinado: sesión válida y board no bloqueado + const writable = async (): Promise => { + const s = guard(); + if (!s) return null; + if (await isLocked(s.boardId)) return null; + return s; + }; + + // ---------- notas ---------- + socket.on("note:add", async (p: { columnId: string; text: string; color?: string }) => { + const s = await writable(); + if (!s) return; + await db.insert(schema.notes).values({ + id: nanoid(), + boardId: s.boardId, + columnId: p.columnId, + authorId: s.participantId, + authorName: s.name, + text: p.text ?? "", + color: typeof p.color === "string" ? p.color.slice(0, 16) : null, + position: Date.now(), // ordenación por creación + }); + await broadcast(io, s.boardId); + }); + + socket.on("note:update", async (p: { noteId: string; text: string }) => { + const s = await writable(); + if (!s) return; + await db + .update(schema.notes) + .set({ text: p.text }) + .where(and(eq(schema.notes.id, p.noteId), eq(schema.notes.authorId, s.participantId))); + await broadcast(io, s.boardId); + }); + + socket.on("note:delete", async (p: { noteId: string }) => { + const s = await writable(); + if (!s) return; + const [note] = await db.select().from(schema.notes).where(eq(schema.notes.id, p.noteId)); + if (!note || note.authorId !== s.participantId) return; + await db.delete(schema.votes).where(eq(schema.votes.targetId, p.noteId)); + await db.delete(schema.notes).where(eq(schema.notes.id, p.noteId)); + if (note.groupId) await deleteGroupIfEmpty(note.groupId); + await broadcast(io, s.boardId); + }); + + // el autor publica/despublica su propia nota + socket.on("note:reveal", async (p: { noteId: string; revealed: boolean }) => { + const s = await writable(); + if (!s) return; + await db + .update(schema.notes) + .set({ revealed: p.revealed }) + .where(and(eq(schema.notes.id, p.noteId), eq(schema.notes.authorId, s.participantId))); + await broadcast(io, s.boardId); + }); + + // cambiar mi color: recolorea TODAS mis notas (y vale para las futuras en el cliente) + socket.on("note:recolorMine", async (p: { color: string }) => { + const s = await writable(); + if (!s) return; + const color = typeof p.color === "string" ? p.color.slice(0, 16) : null; + if (!color) return; + await db + .update(schema.notes) + .set({ color }) + .where(and(eq(schema.notes.boardId, s.boardId), eq(schema.notes.authorId, s.participantId))); + await broadcast(io, s.boardId); + }); + + // "Publish all": publica todas mis notas (opcionalmente de una columna) + socket.on("note:publishMine", async (p: { columnId?: string }) => { + const s = await writable(); + if (!s) return; + const filters = [eq(schema.notes.boardId, s.boardId), eq(schema.notes.authorId, s.participantId)]; + if (p.columnId) filters.push(eq(schema.notes.columnId, p.columnId)); + await db.update(schema.notes).set({ revealed: true }).where(and(...filters)); + await broadcast(io, s.boardId); + }); + + // colocación unificada: mover de columna, reordenar y entrar/salir de grupo + socket.on( + "note:move", + async (p: { noteId: string; columnId: string; groupId?: string | null; position: number }) => { + const s = await writable(); + if (!s) return; + const [note] = await db.select().from(schema.notes).where(eq(schema.notes.id, p.noteId)); + if (!note) return; + const newGroup = p.groupId ?? null; + await db + .update(schema.notes) + // entrar en un grupo implica publicar la nota (agrupar es acción pública) + .set({ columnId: p.columnId, groupId: newGroup, position: p.position, ...(newGroup ? { revealed: true } : {}) }) + .where(eq(schema.notes.id, p.noteId)); + if (newGroup !== note.groupId) { + // cambia su estado de agrupación => sus votos sueltos dejan de aplicar + await db.delete(schema.votes).where(eq(schema.votes.targetId, p.noteId)); + if (note.groupId) await deleteGroupIfEmpty(note.groupId); + } + await broadcast(io, s.boardId); + }, + ); + + // ---------- grupos ---------- + // soltar una nota sobre otra: crea grupo (o se une al de la nota destino) + socket.on("group:create", async (p: { noteId: string; targetNoteId: string }) => { + const s = await writable(); + if (!s) return; + if (p.noteId === p.targetNoteId) return; + const [target] = await db.select().from(schema.notes).where(eq(schema.notes.id, p.targetNoteId)); + const [dragged] = await db.select().from(schema.notes).where(eq(schema.notes.id, p.noteId)); + if (!target || !dragged) return; + + let groupId = target.groupId; + if (!groupId) { + groupId = nanoid(); + await db.insert(schema.groups).values({ + id: groupId, + boardId: s.boardId, + columnId: target.columnId, + title: target.text.slice(0, 60), // por defecto, el texto de la primera nota (la ancla) + position: target.position, + }); + await db + .update(schema.notes) + .set({ groupId, position: 0, revealed: true }) + .where(eq(schema.notes.id, target.id)); + } + const oldGroup = dragged.groupId; + await db + .update(schema.notes) + .set({ groupId, columnId: target.columnId, position: Date.now(), revealed: true }) + .where(eq(schema.notes.id, dragged.id)); + + // votar el grupo es la unidad => reiniciamos votos sueltos de las notas implicadas + await db.delete(schema.votes).where(eq(schema.votes.targetId, target.id)); + await db.delete(schema.votes).where(eq(schema.votes.targetId, dragged.id)); + if (oldGroup && oldGroup !== groupId) await deleteGroupIfEmpty(oldGroup); + await broadcast(io, s.boardId); + }); + + socket.on("group:rename", async (p: { groupId: string; title: string }) => { + const s = await writable(); + if (!s) return; + await db.update(schema.groups).set({ title: p.title }).where(eq(schema.groups.id, p.groupId)); + await broadcast(io, s.boardId); + }); + + socket.on("group:move", async (p: { groupId: string; columnId: string; position: number }) => { + const s = await writable(); + if (!s) return; + await db + .update(schema.groups) + .set({ columnId: p.columnId, position: p.position }) + .where(eq(schema.groups.id, p.groupId)); + // las notas del grupo siguen al grupo de columna + await db + .update(schema.notes) + .set({ columnId: p.columnId }) + .where(eq(schema.notes.groupId, p.groupId)); + await broadcast(io, s.boardId); + }); + + socket.on("group:delete", async (p: { groupId: string }) => { + const s = await writable(); + if (!s) return; + await db.update(schema.notes).set({ groupId: null }).where(eq(schema.notes.groupId, p.groupId)); + await db.delete(schema.votes).where(eq(schema.votes.targetId, p.groupId)); + await db.delete(schema.groups).where(eq(schema.groups.id, p.groupId)); + await broadcast(io, s.boardId); + }); + + // ---------- votos ---------- + // presupuesto usado por el participante = suma de contadores + const usedVotes = (tx: Pick, boardId: string, participantId: string) => + tx + .select({ used: sql`coalesce(sum(${schema.votes.count}), 0)::int` }) + .from(schema.votes) + .where(and(eq(schema.votes.boardId, boardId), eq(schema.votes.participantId, participantId))) + .then((r) => r[0]?.used ?? 0); + + // añade un voto (apila si allowMultiVote); respeta el cupo total por persona + socket.on("vote:add", async (p: { targetType?: string; targetId?: string; noteId?: string }) => { + const s = await writable(); + if (!s) return; + const targetType = p.targetType ?? "note"; + const targetId = p.targetId ?? p.noteId; + if (!targetId) return; + await db.transaction(async (tx) => { + const [board] = await tx + .select() + .from(schema.boards) + .where(eq(schema.boards.id, s.boardId)) + .for("update"); + if (!board) return; + if ((await usedVotes(tx, s.boardId, s.participantId)) >= board.votesPerUser) return; // cupo agotado + const [row] = await tx + .select() + .from(schema.votes) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + if (row) { + if (!board.allowMultiVote) return; // ya votado y no se permite apilar + await tx + .update(schema.votes) + .set({ count: row.count + 1 }) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + } else { + await tx.insert(schema.votes).values({ + boardId: s.boardId, + targetType, + targetId, + participantId: s.participantId, + count: 1, + }); + } + }); + await broadcast(io, s.boardId); + }); + + // quita un voto (decrementa; borra la fila al llegar a 0) + socket.on("vote:remove", async (p: { targetId?: string; noteId?: string }) => { + const s = await writable(); + if (!s) return; + const targetId = p.targetId ?? p.noteId; + if (!targetId) return; + const [row] = await db + .select() + .from(schema.votes) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + if (!row) return; + if (row.count > 1) { + await db + .update(schema.votes) + .set({ count: row.count - 1 }) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + } else { + await db + .delete(schema.votes) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + } + await broadcast(io, s.boardId); + }); + + // legacy: alterna por completo el voto de un ítem (todo/nada) + socket.on("vote:toggle", async (p: { targetType?: string; targetId?: string; noteId?: string }) => { + const s = await writable(); + if (!s) return; + const targetType = p.targetType ?? "note"; + const targetId = p.targetId ?? p.noteId; + if (!targetId) return; + await db.transaction(async (tx) => { + const [board] = await tx + .select() + .from(schema.boards) + .where(eq(schema.boards.id, s.boardId)) + .for("update"); + if (!board) return; + const [row] = await tx + .select() + .from(schema.votes) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + if (row) { + await tx + .delete(schema.votes) + .where(and(eq(schema.votes.targetId, targetId), eq(schema.votes.participantId, s.participantId))); + return; + } + if ((await usedVotes(tx, s.boardId, s.participantId)) >= board.votesPerUser) return; + await tx + .insert(schema.votes) + .values({ boardId: s.boardId, targetType, targetId, participantId: s.participantId, count: 1 }); + }); + await broadcast(io, s.boardId); + }); + + // ---------- ajustes del board (moderador) ---------- + socket.on( + "board:update", + async (p: { + title?: string; + votesPerUser?: number; + allowMultiVote?: boolean; + moderationMode?: string; + showCardCreators?: boolean; + locked?: boolean; + moderatorToken?: string; + }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + const set: Record = {}; + if (typeof p.title === "string" && p.title.trim()) set.title = p.title.trim(); + if (typeof p.votesPerUser === "number") set.votesPerUser = Math.max(0, Math.min(50, Math.round(p.votesPerUser))); + if (typeof p.allowMultiVote === "boolean") set.allowMultiVote = p.allowMultiVote; + if (p.moderationMode === "everyone" || p.moderationMode === "single") set.moderationMode = p.moderationMode; + if (typeof p.showCardCreators === "boolean") set.showCardCreators = p.showCardCreators; + if (typeof p.locked === "boolean") set.locked = p.locked; + if (Object.keys(set).length) await db.update(schema.boards).set(set).where(eq(schema.boards.id, s.boardId)); + await broadcast(io, s.boardId); + }, + ); + + // reclamar la retro: pasa a moderación 'single' con un token nuevo solo para ti + socket.on("board:claim", async (p: { moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + const token = nanoid(24); + await db + .update(schema.boards) + .set({ moderationMode: "single", moderatorToken: token }) + .where(eq(schema.boards.id, s.boardId)); + const sess = sessions.get(socket.id); + if (sess) sess.moderatorToken = token; + socket.emit("board:claimed", { moderatorToken: token }); + await broadcast(io, s.boardId); + }); + + // quitar todos los votos del board (moderador) + socket.on("votes:clear", async (p: { moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + await db.delete(schema.votes).where(eq(schema.votes.boardId, s.boardId)); + await broadcast(io, s.boardId); + }); + + // borrar la retro (moderador) + socket.on("board:remove", async (p: { moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + await db.delete(schema.boards).where(eq(schema.boards.id, s.boardId)); + io.to(s.boardId).emit("board:destroyed"); + }); + + // ---------- columnas (moderador) ---------- + socket.on("column:add", async (p: { title?: string; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + const [{ max }] = await db + .select({ max: sql`coalesce(max(${schema.columns.position}), -1)::int` }) + .from(schema.columns) + .where(eq(schema.columns.boardId, s.boardId)); + await db.insert(schema.columns).values({ + id: nanoid(), + boardId: s.boardId, + title: (p.title ?? "").trim() || "Nueva columna", + position: max + 1, + }); + await broadcast(io, s.boardId); + }); + + socket.on("column:rename", async (p: { columnId: string; title: string; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + const title = (p.title ?? "").trim(); + if (!title) return; + await db + .update(schema.columns) + .set({ title }) + .where(and(eq(schema.columns.id, p.columnId), eq(schema.columns.boardId, s.boardId))); + await broadcast(io, s.boardId); + }); + + socket.on("column:delete", async (p: { columnId: string; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + // no dejar el board sin columnas + const cols = await db.select({ id: schema.columns.id }).from(schema.columns).where(eq(schema.columns.boardId, s.boardId)); + if (cols.length <= 1) return; + await db + .delete(schema.columns) + .where(and(eq(schema.columns.id, p.columnId), eq(schema.columns.boardId, s.boardId))); + await broadcast(io, s.boardId); + }); + + // reordenar: el cliente envía el orden deseado de ids + socket.on("column:reorder", async (p: { orderedIds: string[]; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + if (!Array.isArray(p.orderedIds)) return; + await db.transaction(async (tx) => { + for (let i = 0; i < p.orderedIds.length; i++) { + await tx + .update(schema.columns) + .set({ position: i }) + .where(and(eq(schema.columns.id, p.orderedIds[i]), eq(schema.columns.boardId, s.boardId))); + } + }); + await broadcast(io, s.boardId); + }); + + // cambiar mi propio nombre (actualiza la autoría de mis notas) + socket.on("participant:rename", async (p: { name: string }) => { + const s = guard(); + if (!s) return; + const name = (p.name ?? "").trim(); + if (!name) return; + s.name = name; + await db + .update(schema.notes) + .set({ authorName: name }) + .where(and(eq(schema.notes.boardId, s.boardId), eq(schema.notes.authorId, s.participantId))); + await broadcast(io, s.boardId); + }); + + socket.on("board:reveal", async (p: { revealed: boolean; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + await db.update(schema.boards).set({ revealed: p.revealed }).where(eq(schema.boards.id, s.boardId)); + await broadcast(io, s.boardId); + }); + + socket.on("board:revealVotes", async (p: { revealed: boolean; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + await db.update(schema.boards).set({ votesRevealed: p.revealed }).where(eq(schema.boards.id, s.boardId)); + await broadcast(io, s.boardId); + }); + + // fase de la retro (Lluvia/Votación/Discusión), compartida (moderador) + socket.on("board:phase", async (p: { phase: string; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + if (!["writing", "voting", "done", "revealed"].includes(p.phase)) return; + await db.update(schema.boards).set({ phase: p.phase }).where(eq(schema.boards.id, s.boardId)); + await broadcast(io, s.boardId); + }); + + socket.on("timer:start", async (p: { seconds: number; moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + const endsAt = new Date(Date.now() + Math.max(1, p.seconds) * 1000); + await db + .update(schema.boards) + .set({ timerEndsAt: endsAt, timerRunning: true }) + .where(eq(schema.boards.id, s.boardId)); + await broadcast(io, s.boardId); + }); + + socket.on("timer:stop", async (p: { moderatorToken?: string }) => { + const s = guard(); + if (!s) return; + if (!(await requireMod(s.boardId, p.moderatorToken))) return socket.emit("error", { message: "forbidden" }); + await db.update(schema.boards).set({ timerRunning: false }).where(eq(schema.boards.id, s.boardId)); + await broadcast(io, s.boardId); + }); + + // ---------- action items ---------- + socket.on("actionitem:add", async (p: { text: string }) => { + const s = guard(); + if (!s) return; + await db.insert(schema.actionItems).values({ id: nanoid(), boardId: s.boardId, text: p.text, owner: null }); + await broadcast(io, s.boardId); + }); + + socket.on("actionitem:update", async (p: { id: string; text?: string; owner?: string | null }) => { + const s = guard(); + if (!s) return; + const set: Record = {}; + if (p.text !== undefined) set.text = p.text; + if (p.owner !== undefined) set.owner = p.owner; + await db.update(schema.actionItems).set(set).where(eq(schema.actionItems.id, p.id)); + await broadcast(io, s.boardId); + }); + + socket.on("actionitem:delete", async (p: { id: string }) => { + const s = guard(); + if (!s) return; + await db.delete(schema.actionItems).where(eq(schema.actionItems.id, p.id)); + await broadcast(io, s.boardId); + }); + + socket.on("actionitem:toggle", async (p: { id: string; done: boolean }) => { + const s = guard(); + if (!s) return; + await db.update(schema.actionItems).set({ done: p.done }).where(eq(schema.actionItems.id, p.id)); + await broadcast(io, s.boardId); + }); + }); +} + +// Borra un grupo si se ha quedado sin notas (y sus votos). +async function deleteGroupIfEmpty(groupId: string) { + const [{ count }] = await db + .select({ count: sql`count(*)::int` }) + .from(schema.notes) + .where(eq(schema.notes.groupId, groupId)); + if (count === 0) { + await db.delete(schema.votes).where(eq(schema.votes.targetId, groupId)); + await db.delete(schema.groups).where(eq(schema.groups.id, groupId)); + } +} + +// Difunde a cada socket de la room su propia vista: oculta notas/votos si procede, +// e inyecta la presencia y si ese viewer es moderador. +async function broadcast(io: Server, boardId: string) { + const state = await loadBoardState(boardId); + if (!state) return; + // token del moderador (secreto, no va en el estado público): solo para comparar + const [board] = await db + .select({ token: schema.boards.moderatorToken }) + .from(schema.boards) + .where(eq(schema.boards.id, boardId)); + const modToken = board?.token; + + const socketsInRoom = await io.in(boardId).fetchSockets(); + + // presencia: participantes conectados, deduplicados por participantId + const byId = new Map(); + for (const s of socketsInRoom) { + const sess = sessions.get(s.id); + if (sess) byId.set(sess.participantId, sess.name); + } + const participants = [...byId].map(([id, name]) => ({ id, name })); + + for (const s of socketsInRoom) { + const sess = sessions.get(s.id); + const pid = sess?.participantId; + const view = pid ? viewFor(state, pid) : state; + const youAreModerator = + state.moderationMode === "everyone" || (!!sess?.moderatorToken && sess.moderatorToken === modToken); + s.emit("board:state", { ...view, participants, youAreModerator }); + } +} diff --git a/server/src/templates.ts b/server/src/templates.ts new file mode 100644 index 0000000..b72fa1c --- /dev/null +++ b/server/src/templates.ts @@ -0,0 +1,48 @@ +// Plantillas predefinidas: cada una es una lista ordenada de columnas. +export type Template = { + id: string; + name: string; + columns: string[]; +}; + +export const TEMPLATES: Template[] = [ + { + id: "mad-sad-glad", + name: "Mad / Sad / Glad", + columns: ["😠 Mad", "😢 Sad", "😄 Glad"], + }, + { + id: "start-stop-continue", + name: "Start / Stop / Continue", + columns: ["🟢 Start", "🔴 Stop", "🔵 Continue"], + }, + { + id: "4ls", + name: "4 Ls", + columns: ["👍 Liked", "📚 Learned", "🕳️ Lacked", "🎯 Longed for"], + }, + { + id: "went-well", + name: "Funcionó / No funcionó / Acciones", + columns: ["✅ Funcionó", "⚠️ No funcionó", "💡 Acciones"], + }, + { + id: "sailboat", + name: "Velero (Sailboat)", + columns: ["⛵ Viento", "⚓ Ancla", "🪨 Rocas", "🏝️ Meta"], + }, + { + id: "daki", + name: "DAKI", + columns: ["🗑️ Drop", "➕ Add", "📌 Keep", "📈 Improve"], + }, + { + id: "blank", + name: "En blanco (columnas libres)", + columns: ["Columna 1", "Columna 2", "Columna 3"], + }, +]; + +export function getTemplate(id: string): Template | undefined { + return TEMPLATES.find((t) => t.id === id); +} diff --git a/server/test/cleanup.test.ts b/server/test/cleanup.test.ts new file mode 100644 index 0000000..ab3569e --- /dev/null +++ b/server/test/cleanup.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { eq } from "drizzle-orm"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; +import { runCleanup } from "../src/app.js"; +import { db, schema } from "../src/db/index.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("Autodestrucción", () => { + it("borra boards caducados y avisa a la room", async () => { + const board = await createBoard(server.url, { ttlHours: 1 }); + // forzamos expires_at al pasado + await db + .update(schema.boards) + .set({ expiresAt: new Date(Date.now() - 1000) }) + .where(eq(schema.boards.id, board.boardId)); + + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const destroyed = new Promise((resolve) => + alice.socket.once("board:destroyed", () => resolve()), + ); + + const deleted = await runCleanup(server.io); + expect(deleted).toContain(board.boardId); + await destroyed; // la room recibió el aviso + + // ya no existe en DB + const rows = await db.select().from(schema.boards).where(eq(schema.boards.id, board.boardId)); + expect(rows).toHaveLength(0); + + alice.socket.close(); + }); + + it("no borra boards sin caducidad ni los aún vigentes", async () => { + const persistent = await createBoard(server.url, { ttlHours: null }); + const future = await createBoard(server.url, { ttlHours: 24 }); + + const deleted = await runCleanup(server.io); + expect(deleted).not.toContain(persistent.boardId); + expect(deleted).not.toContain(future.boardId); + }); +}); diff --git a/server/test/columns.test.ts b/server/test/columns.test.ts new file mode 100644 index 0000000..32d3e84 --- /dev/null +++ b/server/test/columns.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("Columnas y color", () => { + it("recolorear cambia el color de TODAS mis notas", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + alice.emit("note:add", { columnId: colId, text: "a", color: "#ffe08a" }); + alice.emit("note:add", { columnId: colId, text: "b", color: "#ffe08a" }); + await alice.waitFor((s) => s.notes.length === 2); + + alice.emit("note:recolorMine", { color: "#b9ddf5" }); + const recolored = await alice.waitFor((s) => s.notes.every((n) => n.color === "#b9ddf5")); + expect(recolored.notes.map((n) => n.color)).toEqual(["#b9ddf5", "#b9ddf5"]); + + alice.close(); + }); + + it("añadir, renombrar, reordenar y eliminar columnas (moderador)", async () => { + const board = await createBoard(server.url); // mad-sad-glad => 3 columnas + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const tok = board.moderatorToken; + + // añadir + alice.emit("column:add", { title: "Extra", moderatorToken: tok }); + let st = await alice.waitFor((s) => s.columns.length === 4); + const extra = st.columns.find((c) => c.title === "Extra")!; + expect(extra).toBeTruthy(); + + // renombrar + alice.emit("column:rename", { columnId: extra.id, title: "Renombrada", moderatorToken: tok }); + st = await alice.waitFor((s) => s.columns.some((c) => c.id === extra.id && c.title === "Renombrada")); + expect(st.columns.find((c) => c.id === extra.id)!.title).toBe("Renombrada"); + + // reordenar: poner la última primera + const ids = st.columns.map((c) => c.id); + const reordered = [ids[ids.length - 1], ...ids.slice(0, -1)]; + alice.emit("column:reorder", { orderedIds: reordered, moderatorToken: tok }); + st = await alice.waitFor((s) => s.columns[0].id === reordered[0]); + expect(st.columns.map((c) => c.id)).toEqual(reordered); + + // eliminar + alice.emit("column:delete", { columnId: extra.id, moderatorToken: tok }); + st = await alice.waitFor((s) => s.columns.length === 3); + expect(st.columns.some((c) => c.id === extra.id)).toBe(false); + + alice.close(); + }); + + it("la fase se comparte (board:phase)", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + expect(alice.last!.phase).toBe("writing"); + + alice.emit("board:phase", { phase: "voting", moderatorToken: board.moderatorToken }); + const bobState = await bob.waitFor((s) => s.phase === "voting"); + expect(bobState.phase).toBe("voting"); + + alice.close(); + bob.close(); + }); +}); diff --git a/server/test/global-setup.ts b/server/test/global-setup.ts new file mode 100644 index 0000000..d0d8f34 --- /dev/null +++ b/server/test/global-setup.ts @@ -0,0 +1,96 @@ +import postgres from "postgres"; + +// DDL self-contained: crea la base de datos de test desde cero (idempotente). +const DDL = ` +DROP TABLE IF EXISTS action_items, votes, notes, groups, columns, boards CASCADE; + +CREATE TABLE boards ( + id text PRIMARY KEY, + title text NOT NULL, + phase text NOT NULL DEFAULT 'writing', + revealed boolean NOT NULL DEFAULT false, + votes_revealed boolean NOT NULL DEFAULT false, + votes_per_user integer NOT NULL DEFAULT 3, + allow_multi_vote boolean NOT NULL DEFAULT true, + moderation_mode text NOT NULL DEFAULT 'everyone', + show_card_creators boolean NOT NULL DEFAULT true, + locked boolean NOT NULL DEFAULT false, + timer_ends_at timestamptz, + timer_running boolean NOT NULL DEFAULT false, + moderator_token text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + expires_at timestamptz +); + +CREATE TABLE columns ( + id text PRIMARY KEY, + board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + title text NOT NULL, + position integer NOT NULL +); + +CREATE TABLE groups ( + id text PRIMARY KEY, + board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + column_id text NOT NULL REFERENCES columns(id) ON DELETE CASCADE, + title text NOT NULL DEFAULT '', + position double precision NOT NULL DEFAULT 0 +); + +CREATE TABLE notes ( + id text PRIMARY KEY, + board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + column_id text NOT NULL REFERENCES columns(id) ON DELETE CASCADE, + group_id text REFERENCES groups(id) ON DELETE SET NULL, + author_id text NOT NULL, + author_name text NOT NULL, + text text NOT NULL, + color text, + revealed boolean NOT NULL DEFAULT false, + position double precision NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE votes ( + board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + target_type text NOT NULL, + target_id text NOT NULL, + participant_id text NOT NULL, + count integer NOT NULL DEFAULT 1 +); +CREATE UNIQUE INDEX votes_target_participant_uniq ON votes (target_id, participant_id); + +CREATE TABLE action_items ( + id text PRIMARY KEY, + board_id text NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + text text NOT NULL, + owner text, + done boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now() +); +`; + +export async function setup() { + // globalSetup no recibe `test.env`; replicamos el default de vitest.config.ts. + const url = + process.env.TEST_DATABASE_URL ?? + process.env.DATABASE_URL ?? + "postgres://retro:retro@localhost:5433/retro_test"; + // Asegura que exista la base de datos de test (crea retro_test si falta). + const dbName = new URL(url).pathname.slice(1); + const adminUrl = url.replace(`/${dbName}`, "/postgres"); + const admin = postgres(adminUrl, { max: 1 }); + try { + const exists = await admin`SELECT 1 FROM pg_database WHERE datname = ${dbName}`; + if (exists.length === 0) await admin.unsafe(`CREATE DATABASE ${dbName}`); + } finally { + await admin.end(); + } + + const sql = postgres(url, { max: 1 }); + try { + await sql.unsafe(DDL); + } finally { + await sql.end(); + } +} diff --git a/server/test/groups.test.ts b/server/test/groups.test.ts new file mode 100644 index 0000000..5f248b0 --- /dev/null +++ b/server/test/groups.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +async function twoNotes(boardId: string, url: string) { + const alice = await join(url, boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + alice.emit("note:add", { columnId: colId, text: "A" }); + alice.emit("note:add", { columnId: colId, text: "B" }); + const st = await alice.waitFor((s) => s.notes.length === 2); + const [a, b] = st.notes; + return { alice, colId, a, b }; +} + +describe("Grupos", () => { + it("agrupar dos notas crea un grupo votable", async () => { + const board = await createBoard(server.url); + const { alice, a, b } = await twoNotes(board.boardId, server.url); + + alice.emit("group:create", { noteId: b.id, targetNoteId: a.id }); + const grouped = await alice.waitFor((s) => s.groups.length === 1); + const groupId = grouped.groups[0].id; + expect(grouped.notes.every((n) => n.groupId === groupId)).toBe(true); + + // votar el grupo como unidad + alice.emit("vote:toggle", { targetType: "group", targetId: groupId }); + const voted = await alice.waitFor((s) => s.votes.some((v) => v.targetId === groupId)); + expect(voted.votes.filter((v) => v.targetId === groupId)).toHaveLength(1); + + alice.close(); + }); + + it("al agrupar se reinician los votos sueltos de las notas", async () => { + const board = await createBoard(server.url); + const { alice, a, b } = await twoNotes(board.boardId, server.url); + + alice.emit("vote:toggle", { targetType: "note", targetId: a.id }); + await alice.waitFor((s) => s.votes.some((v) => v.targetId === a.id)); + + alice.emit("group:create", { noteId: b.id, targetNoteId: a.id }); + const after = await alice.waitFor((s) => s.groups.length === 1); + expect(after.votes.filter((v) => v.targetId === a.id)).toHaveLength(0); + + alice.close(); + }); + + it("sacar la última nota de un grupo lo elimina", async () => { + const board = await createBoard(server.url); + const { alice, colId, a, b } = await twoNotes(board.boardId, server.url); + + alice.emit("group:create", { noteId: b.id, targetNoteId: a.id }); + const grouped = await alice.waitFor((s) => s.groups.length === 1); + expect(grouped.groups).toHaveLength(1); + + // saca una nota: el grupo sigue (1 miembro) + alice.emit("note:move", { noteId: a.id, columnId: colId, groupId: null, position: 5 }); + await alice.waitFor((s) => s.notes.filter((n) => n.groupId).length === 1); + + // saca la última: el grupo se borra + alice.emit("note:move", { noteId: b.id, columnId: colId, groupId: null, position: 6 }); + const empty = await alice.waitFor((s) => s.groups.length === 0); + expect(empty.groups).toHaveLength(0); + + alice.close(); + }); +}); diff --git a/server/test/helpers.ts b/server/test/helpers.ts new file mode 100644 index 0000000..ed24074 --- /dev/null +++ b/server/test/helpers.ts @@ -0,0 +1,85 @@ +import { buildServer, type BuiltServer } from "../src/app.js"; +import { io as ioc, type Socket } from "socket.io-client"; +import type { BoardState } from "../src/room.js"; + +export type TestServer = BuiltServer & { port: number; url: string }; + +export async function startTestServer(): Promise { + const server = buildServer({ cleanup: false }); + const port = await server.listen(0); // puerto efímero + return Object.assign(server, { port, url: `http://localhost:${port}` }); +} + +export async function createBoard( + url: string, + body: Record = {}, +): Promise<{ boardId: string; moderatorToken: string; expiresAt: string | null }> { + const res = await fetch(`${url}/api/boards`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ templateId: "mad-sad-glad", votesPerUser: 3, ...body }), + }); + return res.json(); +} + +// Cliente de test: mantiene el último estado recibido para evitar carreras +// (el estado buscado puede haber llegado antes de adjuntar el listener). +export class Client { + last: BoardState | null = null; + constructor(public socket: Socket) { + socket.on("board:state", (s: BoardState) => (this.last = s)); + } + + emit(event: string, payload: Record) { + this.socket.emit(event, payload); + } + + // Resuelve cuando el último estado (presente o futuro) cumple el predicado. + waitFor(predicate: (s: BoardState) => boolean, timeoutMs = 10000): Promise { + if (this.last && predicate(this.last)) return Promise.resolve(this.last); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.socket.off("board:state", handler); + reject(new Error("waitFor timeout")); + }, timeoutMs); + const handler = (s: BoardState) => { + if (predicate(s)) { + clearTimeout(timer); + this.socket.off("board:state", handler); + resolve(s); + } + }; + this.socket.on("board:state", handler); + }); + } + + close() { + this.socket.close(); + } +} + +// Conecta, hace join y devuelve un Client ya con su primer estado. +export function join( + url: string, + boardId: string, + participantId: string, + name: string, +): Promise { + return new Promise((resolve, reject) => { + const socket = ioc(url, { transports: ["websocket"], forceNew: true }); + const client = new Client(socket); + const timer = setTimeout(() => reject(new Error("join timeout")), 10000); + socket.on("connect", () => socket.emit("board:join", { boardId, participantId, name })); + socket.once("error", (e) => { + clearTimeout(timer); + reject(new Error(`join error: ${JSON.stringify(e)}`)); + }); + const ready = (s: BoardState) => { + clearTimeout(timer); + socket.off("board:state", ready); + client.last = s; + resolve(client); + }; + socket.on("board:state", ready); + }); +} diff --git a/server/test/import.test.ts b/server/test/import.test.ts new file mode 100644 index 0000000..d4bfb57 --- /dev/null +++ b/server/test/import.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +const snapshot = { + version: 1, + board: { title: "Importado", votesPerUser: 4 }, + columns: [ + { id: "c1", title: "Columna A", position: 0 }, + { id: "c2", title: "Columna B", position: 1 }, + ], + groups: [], + notes: [ + { + id: "n1", + columnId: "c1", + groupId: null, + authorId: "x", + authorName: "X", + text: "nota importada", + revealed: true, + position: 1, + }, + ], + votes: [{ targetType: "note", targetId: "n1", participantId: "x" }], + actionItems: [{ text: "tarea importada", owner: "X" }], +}; + +async function importAndExport(mode: "full" | "structure") { + const res = await fetch(`${server.url}/api/boards/import`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ snapshot, mode }), + }); + const { boardId } = await res.json(); + const md = await (await fetch(`${server.url}/api/boards/${boardId}/export`)).text(); + return { boardId, md }; +} + +describe("Import", () => { + it("modo 'full' importa columnas, notas y votos", async () => { + const { md } = await importAndExport("full"); + expect(md).toContain("# Importado"); + expect(md).toContain("## Columna A"); + expect(md).toContain("nota importada — _X_ (1 👍)"); + expect(md).toContain("tarea importada"); + }); + + it("modo 'structure' importa solo columnas y config, sin notas", async () => { + const { md } = await importAndExport("structure"); + expect(md).toContain("## Columna A"); + expect(md).toContain("## Columna B"); + expect(md).not.toContain("nota importada"); + expect(md).not.toContain("tarea importada"); + }); + + it("rechaza un snapshot sin columnas", async () => { + const res = await fetch(`${server.url}/api/boards/import`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ snapshot: { version: 1, columns: [] }, mode: "full" }), + }); + expect(res.status).toBe(400); + }); +}); + +describe("Export con opciones", () => { + async function importFull(): Promise { + const res = await fetch(`${server.url}/api/boards/import`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ snapshot, mode: "full" }), + }); + return (await res.json()).boardId; + } + const md = (id: string, qs: string) => + fetch(`${server.url}/api/boards/${id}/export${qs}`).then((r) => r.text()); + + it("autores y votos se pueden desactivar por query", async () => { + const id = await importFull(); + expect(await md(id, "")).toContain("nota importada — _X_ (1 👍)"); // defaults + const noAuthors = await md(id, "?authors=0"); + expect(noAuthors).toContain("nota importada (1 👍)"); + expect(noAuthors).not.toContain("_X_"); + const noVotes = await md(id, "?votes=0"); + expect(noVotes).not.toContain("👍"); + }); + + it("la fecha de creación se añade al título con created=1", async () => { + const id = await importFull(); + const withDate = await md(id, "?created=1"); + expect(withDate).toMatch(/^# \d+\/\d+\/\d+ - Importado/); + }); +}); diff --git a/server/test/multivote.test.ts b/server/test/multivote.test.ts new file mode 100644 index 0000000..b759810 --- /dev/null +++ b/server/test/multivote.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +const myCount = (state: { votes: { targetId: string; participantId: string; count: number }[] }, targetId: string) => + state.votes + .filter((v) => v.targetId === targetId && v.participantId === "alice") + .reduce((s, v) => s + v.count, 0); + +describe("Multivoto", () => { + it("apila votos en el mismo ítem hasta el cupo y deja quitar", async () => { + const board = await createBoard(server.url, { votesPerUser: 3 }); // allowMultiVote por defecto: true + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + alice.emit("note:add", { columnId: colId, text: "favorita" }); + const noteId = (await alice.waitFor((s) => s.notes.length === 1)).notes[0].id; + + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + const stacked = await alice.waitFor((s) => myCount(s, noteId) === 3); + expect(myCount(stacked, noteId)).toBe(3); + + // 4º voto: cupo agotado, no sube + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + await new Promise((r) => setTimeout(r, 200)); + expect(myCount(alice.last!, noteId)).toBe(3); + + // quitar uno + alice.emit("vote:remove", { targetId: noteId }); + const afterRemove = await alice.waitFor((s) => myCount(s, noteId) === 2); + expect(myCount(afterRemove, noteId)).toBe(2); + + alice.close(); + }); + + it("con allowMultiVote=false no se puede apilar en el mismo ítem", async () => { + const board = await createBoard(server.url, { votesPerUser: 5 }); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + alice.emit("board:update", { allowMultiVote: false, moderatorToken: board.moderatorToken }); + await alice.waitFor((s) => s.allowMultiVote === false); + + const colId = alice.last!.columns[0].id; + alice.emit("note:add", { columnId: colId, text: "uno" }); + const noteId = (await alice.waitFor((s) => s.notes.length === 1)).notes[0].id; + + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + const after = await alice.waitFor((s) => myCount(s, noteId) >= 1); + await new Promise((r) => setTimeout(r, 200)); + expect(myCount(after, noteId)).toBe(1); + + alice.close(); + }); +}); diff --git a/server/test/options.test.ts b/server/test/options.test.ts new file mode 100644 index 0000000..64f8547 --- /dev/null +++ b/server/test/options.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("Moderación y opciones", () => { + it("por defecto cualquiera modera; al reclamar solo el reclamante", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + + // modo abierto por defecto + expect(alice.last!.moderationMode).toBe("everyone"); + + // Alice reclama (sin token, permitido en modo abierto) y recibe un token nuevo + const claimed = new Promise((resolve) => + alice.socket.once("board:claimed", (p: { moderatorToken: string }) => resolve(p.moderatorToken)), + ); + alice.emit("board:claim", {}); + const token = await claimed; + await alice.waitFor((s) => s.moderationMode === "single"); + + // Bob (sin token) ya no puede revelar; Alice con su token sí + bob.emit("board:reveal", { revealed: true }); + await new Promise((r) => setTimeout(r, 150)); + expect(bob.last!.revealed).toBe(false); + + alice.emit("board:reveal", { revealed: true, moderatorToken: token }); + const revealed = await alice.waitFor((s) => s.revealed === true); + expect(revealed.revealed).toBe(true); + + alice.close(); + bob.close(); + }); + + it("bloquear deja el board en solo lectura (no se añaden notas)", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + + alice.emit("board:update", { locked: true }); // modo abierto: cualquiera modera + await alice.waitFor((s) => s.locked === true); + + alice.emit("note:add", { columnId: colId, text: "no debería entrar" }); + await new Promise((r) => setTimeout(r, 200)); + expect(alice.last!.notes).toHaveLength(0); + + // desbloquear y ahora sí + alice.emit("board:update", { locked: false }); + await alice.waitFor((s) => s.locked === false); + alice.emit("note:add", { columnId: colId, text: "ahora sí" }); + const after = await alice.waitFor((s) => s.notes.length === 1); + expect(after.notes[0].text).toBe("ahora sí"); + + alice.close(); + }); + + it("votes:clear borra todos los votos", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + alice.emit("note:add", { columnId: colId, text: "x" }); + const noteId = (await alice.waitFor((s) => s.notes.length === 1)).notes[0].id; + alice.emit("vote:add", { targetType: "note", targetId: noteId }); + await alice.waitFor((s) => s.votes.length === 1); + + alice.emit("votes:clear", {}); + const cleared = await alice.waitFor((s) => s.votes.length === 0); + expect(cleared.votes).toHaveLength(0); + + alice.close(); + }); + + it("participant:rename actualiza la autoría de mis notas", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + alice.emit("note:add", { columnId: colId, text: "mía" }); + await alice.waitFor((s) => s.notes.length === 1 && s.notes[0].authorName === "Alice"); + + alice.emit("participant:rename", { name: "Alicia" }); + const renamed = await alice.waitFor((s) => s.notes[0]?.authorName === "Alicia"); + expect(renamed.notes[0].authorName).toBe("Alicia"); + expect(renamed.participants.find((p) => p.id === "alice")?.name).toBe("Alicia"); + + alice.close(); + }); + + it("la presencia lista a los participantes conectados", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const st = await alice.waitFor((s) => s.participants.length === 2); + expect(st.participants.map((p) => p.name).sort()).toEqual(["Alice", "Bob"]); + alice.close(); + bob.close(); + }); + + it("action item: marcar hecho", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + alice.emit("actionitem:add", { text: "documentar" }); + const withItem = await alice.waitFor((s) => s.actionItems.length === 1); + const id = withItem.actionItems[0].id; + expect(withItem.actionItems[0].done).toBe(false); + + alice.emit("actionitem:toggle", { id, done: true }); + const done = await alice.waitFor((s) => s.actionItems[0]?.done === true); + expect(done.actionItems[0].done).toBe(true); + + alice.close(); + }); +}); diff --git a/server/test/private.test.ts b/server/test/private.test.ts new file mode 100644 index 0000000..f4c9804 --- /dev/null +++ b/server/test/private.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("Notas privadas y publicar", () => { + it("note:publishMine publica todas mis notas de la columna", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const colId = alice.last!.columns[0].id; + + alice.emit("note:add", { columnId: colId, text: "uno" }); + alice.emit("note:add", { columnId: colId, text: "dos" }); + await alice.waitFor((s) => s.notes.length === 2); + + // Bob recibe las notas pero sin texto (privadas) + const bobBefore = await bob.waitFor((s) => s.notes.length === 2); + expect(bobBefore.notes.every((n) => n.text === "")).toBe(true); + + alice.emit("note:publishMine", { columnId: colId }); + + // guard de longitud: every() es vacuamente true sobre [] -> exigimos las 2 notas + const bobAfter = await bob.waitFor((s) => s.notes.length === 2 && s.notes.every((n) => n.text !== "")); + expect(bobAfter.notes.map((n) => n.text).sort()).toEqual(["dos", "uno"]); + + alice.close(); + bob.close(); + }); + + it("agrupar publica las notas implicadas", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const colId = alice.last!.columns[0].id; + + alice.emit("note:add", { columnId: colId, text: "A" }); + alice.emit("note:add", { columnId: colId, text: "B" }); + const st = await alice.waitFor((s) => s.notes.length === 2); + const [a, b] = st.notes; + + alice.emit("group:create", { noteId: b.id, targetNoteId: a.id }); + + // Bob ve ambas notas con texto (agrupar es público) + const bobAfter = await bob.waitFor( + (s) => s.groups.length === 1 && s.notes.length === 2 && s.notes.every((n) => n.text !== ""), + ); + expect(bobAfter.notes.map((n) => n.text).sort()).toEqual(["A", "B"]); + + alice.close(); + bob.close(); + }); +}); diff --git a/server/test/rest.test.ts b/server/test/rest.test.ts new file mode 100644 index 0000000..b11367f --- /dev/null +++ b/server/test/rest.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("REST", () => { + it("lista plantillas predefinidas", async () => { + const res = await fetch(`${server.url}/api/templates`); + const templates = await res.json(); + expect(Array.isArray(templates)).toBe(true); + expect(templates.find((t: any) => t.id === "mad-sad-glad")).toBeTruthy(); + }); + + it("crea un board desde plantilla y devuelve token de moderador", async () => { + const board = await createBoard(server.url, { title: "Sprint 1" }); + expect(board.boardId).toHaveLength(12); + expect(board.moderatorToken.length).toBeGreaterThan(10); + + const meta = await (await fetch(`${server.url}/api/boards/${board.boardId}`)).json(); + expect(meta.title).toBe("Sprint 1"); + }); + + it("crea un board con columnas libres", async () => { + const board = await createBoard(server.url, { + templateId: undefined, + columns: ["Bien", "Mal", "Acciones"], + }); + const md = await (await fetch(`${server.url}/api/boards/${board.boardId}/export`)).text(); + expect(md).toContain("## Bien"); + expect(md).toContain("## Mal"); + }); + + it("fija expiresAt cuando se pasa ttlHours", async () => { + const board = await createBoard(server.url, { ttlHours: 2 }); + expect(board.expiresAt).toBeTruthy(); + expect(new Date(board.expiresAt!).getTime()).toBeGreaterThan(Date.now()); + }); + + it("devuelve 404 para board inexistente", async () => { + const res = await fetch(`${server.url}/api/boards/noexiste123`); + expect(res.status).toBe(404); + }); + + it("rechaza plantilla inexistente con 400", async () => { + const res = await fetch(`${server.url}/api/boards`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ templateId: "nope" }), + }); + expect(res.status).toBe(400); + }); +}); diff --git a/server/test/reveal-votes.test.ts b/server/test/reveal-votes.test.ts new file mode 100644 index 0000000..a8c7538 --- /dev/null +++ b/server/test/reveal-votes.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("Revelado granular", () => { + it("el autor revela solo su propia nota", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const colId = alice.last!.columns[0].id; + + alice.emit("note:add", { columnId: colId, text: "mía" }); + let bobState = await bob.waitFor((s) => s.notes.length === 1); + expect(bobState.notes[0].text).toBe(""); // oculta + + const noteId = (await alice.waitFor((s) => s.notes.length === 1)).notes[0].id; + alice.emit("note:reveal", { noteId, revealed: true }); + + bobState = await bob.waitFor((s) => s.notes[0]?.text === "mía"); + expect(bobState.notes[0].text).toBe("mía"); + + alice.close(); + bob.close(); + }); + + it("los totales de votos se ocultan hasta que el moderador los revela", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const colId = alice.last!.columns[0].id; + + alice.emit("note:add", { columnId: colId, text: "vota esto" }); + const noteId = (await alice.waitFor((s) => s.notes.length === 1)).notes[0].id; + await bob.waitFor((s) => s.notes.length === 1); + + // Bob vota; Alice NO debe ver el voto de Bob (totales ocultos) + bob.emit("vote:toggle", { targetType: "note", targetId: noteId }); + await bob.waitFor((s) => s.votes.some((v) => v.targetId === noteId)); // Bob ve el suyo + + const aliceView = alice.last!; + expect(aliceView.votes.filter((v) => v.targetId === noteId)).toHaveLength(0); + + // El moderador revela los votos => Alice ve el de Bob + alice.emit("board:revealVotes", { revealed: true, moderatorToken: board.moderatorToken }); + const revealed = await alice.waitFor((s) => s.votesRevealed && s.votes.length === 1); + expect(revealed.votes[0].participantId).toBe("bob"); + + alice.close(); + bob.close(); + }); + + it("cambiar los ajustes del board requiere token de moderador", async () => { + const board = await createBoard(server.url, { votesPerUser: 3 }); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + + // sin token: ignorado + alice.emit("board:update", { title: "Hackeado", votesPerUser: 99 }); + // con token: aplica + alice.emit("board:update", { title: "Renombrado", votesPerUser: 7, moderatorToken: board.moderatorToken }); + + const updated = await alice.waitFor((s) => s.title === "Renombrado"); + expect(updated.votesPerUser).toBe(7); + + alice.close(); + }); +}); diff --git a/server/test/socket.test.ts b/server/test/socket.test.ts new file mode 100644 index 0000000..f29cb02 --- /dev/null +++ b/server/test/socket.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestServer, createBoard, join, type TestServer } from "./helpers.js"; + +let server: TestServer; +beforeAll(async () => { + server = await startTestServer(); +}); +afterAll(async () => { + await server.close(); +}); + +describe("Socket.IO — flujo de retro", () => { + it("oculta las notas de otros hasta revelar", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const colId = alice.last!.columns[0].id; + + alice.emit("note:add", { columnId: colId, text: "secreto de Alice" }); + + const bobState = await bob.waitFor((s) => s.notes.length === 1); + expect(bobState.notes[0].text).toBe(""); // oculta para Bob + expect(bobState.notes[0].authorName).toBe("Alice"); + + const aliceState = await alice.waitFor((s) => s.notes.length === 1); + expect(aliceState.notes[0].text).toBe("secreto de Alice"); // visible para la autora + + alice.close(); + bob.close(); + }); + + it("revela las notas a todos cuando el moderador lo pide", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + const colId = alice.last!.columns[0].id; + + alice.emit("note:add", { columnId: colId, text: "visible tras revelar" }); + await bob.waitFor((s) => s.notes.length === 1); + + // Bob (sin token) no puede revelar; Alice (moderadora) sí + bob.emit("board:reveal", { revealed: true }); + alice.emit("board:reveal", { revealed: true, moderatorToken: board.moderatorToken }); + + const revealed = await bob.waitFor((s) => s.revealed === true); + expect(revealed.notes[0].text).toBe("visible tras revelar"); + + alice.close(); + bob.close(); + }); + + it("respeta el límite de votos por persona", async () => { + const board = await createBoard(server.url, { votesPerUser: 2 }); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const colId = alice.last!.columns[0].id; + + for (const txt of ["a", "b", "c"]) alice.emit("note:add", { columnId: colId, text: txt }); + const withNotes = await alice.waitFor((s) => s.notes.length === 3); + const ids = withNotes.notes.map((n) => n.id); + const mine = (s: { votes: { participantId: string }[] }) => + s.votes.filter((v) => v.participantId === "alice").length; + + // vota las dos primeras (confirmando cada una) + alice.emit("vote:toggle", { noteId: ids[0] }); + await alice.waitFor((s) => mine(s) === 1); + alice.emit("vote:toggle", { noteId: ids[1] }); + await alice.waitFor((s) => mine(s) === 2); + + // el tercer voto se rechaza (cupo = 2) + alice.emit("vote:toggle", { noteId: ids[2] }); + await new Promise((r) => setTimeout(r, 200)); + expect(mine(alice.last!)).toBe(2); + + // quitar un voto libera cupo + alice.emit("vote:toggle", { noteId: ids[0] }); + const afterUnvote = await alice.waitFor((s) => mine(s) === 1); + expect(afterUnvote.votes).toHaveLength(1); + + alice.close(); + }); + + it("comparte el timer (endsAt) con todos los participantes", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + + alice.emit("timer:start", { seconds: 300, moderatorToken: board.moderatorToken }); + const bobState = await bob.waitFor((s) => s.timerRunning); + expect(bobState.timerEndsAt).toBeTruthy(); + expect(new Date(bobState.timerEndsAt!).getTime()).toBeGreaterThan(Date.now()); + + alice.close(); + bob.close(); + }); + + it("crea action items visibles para todos", async () => { + const board = await createBoard(server.url); + const alice = await join(server.url, board.boardId, "alice", "Alice"); + const bob = await join(server.url, board.boardId, "bob", "Bob"); + + alice.emit("actionitem:add", { text: "documentar el deploy" }); + const bobState = await bob.waitFor((s) => s.actionItems.length === 1); + expect(bobState.actionItems[0].text).toBe("documentar el deploy"); + + alice.close(); + bob.close(); + }); +}); diff --git a/server/test/truncate.ts b/server/test/truncate.ts new file mode 100644 index 0000000..e2e3352 --- /dev/null +++ b/server/test/truncate.ts @@ -0,0 +1,9 @@ +import { beforeEach } from "vitest"; +import postgres from "postgres"; + +// Limpia las tablas antes de cada test para aislarlos. +const sql = postgres(process.env.DATABASE_URL!, { max: 1 }); + +beforeEach(async () => { + await sql.unsafe("TRUNCATE action_items, votes, notes, groups, columns, boards CASCADE"); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..495ec33 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/server/vitest.config.ts b/server/vitest.config.ts new file mode 100644 index 0000000..462d365 --- /dev/null +++ b/server/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +const TEST_DB = + process.env.TEST_DATABASE_URL ?? "postgres://retro:retro@localhost:5433/retro_test"; + +export default defineConfig({ + test: { + globalSetup: "./test/global-setup.ts", + setupFiles: "./test/truncate.ts", + env: { DATABASE_URL: TEST_DB }, + fileParallelism: false, // comparten la misma DB; evitamos carreras + hookTimeout: 30_000, + testTimeout: 20_000, + }, +}); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7aab43c --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + Sternboard + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..32c1c2b --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,2074 @@ +{ + "name": "retropentool-web", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "retropentool-web", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^9.0.0", + "@dnd-kit/utilities": "^3.2.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "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/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.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "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.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-9.0.0.tgz", + "integrity": "sha512-3/9r8Mmba0nKTbo8kPnVSFZKf/VSy94nXZ3aUwzPEh78j/LooQ/EFKRZENak4PHKBkN53mgTF/z+Sd8H+FcAnQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.2.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.5.tgz", + "integrity": "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..c8eacd5 --- /dev/null +++ b/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "sternboard-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview --port 5173" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^9.0.0", + "@dnd-kit/utilities": "^3.2.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } +} diff --git a/web/src/components/ActionPointsPanel.tsx b/web/src/components/ActionPointsPanel.tsx new file mode 100644 index 0000000..14a88bc --- /dev/null +++ b/web/src/components/ActionPointsPanel.tsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import { Drawer } from "./Drawer"; +import { useI18n } from "../i18n"; +import type { BoardState } from "../types"; +import type { BoardActions } from "../useBoard"; + +export function ActionPointsPanel({ + state, + actions, + onClose, +}: { + state: BoardState; + actions: BoardActions; + onClose: () => void; +}) { + const { t } = useI18n(); + const [text, setText] = useState(""); + const todo = state.actionItems.filter((a) => !a.done); + const done = state.actionItems.filter((a) => a.done); + + function add() { + if (!text.trim()) return; + actions.addAction(text.trim()); + setText(""); + } + + return ( + +
+ setText(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && add()} + placeholder={t("panel.addActionPoint")} + /> + +
+ +

{t("panel.todo")} ({todo.length})

+
    + {todo.map((a) => ( + + ))} +
+ +

{t("panel.done")} ({done.length})

+
    + {done.map((a) => ( + + ))} +
+
+ ); +} + +function ActionRow({ + item, + actions, +}: { + item: BoardState["actionItems"][number]; + actions: BoardActions; +}) { + const { t } = useI18n(); + return ( +
  • + actions.toggleAction(item.id, e.target.checked)} + /> + e.target.value !== item.text && actions.updateAction(item.id, { text: e.target.value })} + /> + e.target.value !== (item.owner ?? "") && actions.updateAction(item.id, { owner: e.target.value })} + /> + +
  • + ); +} diff --git a/web/src/components/AppBar.tsx b/web/src/components/AppBar.tsx new file mode 100644 index 0000000..d21e48c --- /dev/null +++ b/web/src/components/AppBar.tsx @@ -0,0 +1,19 @@ +import { LangSwitcher } from "../i18n"; +import { ThemeSwitcher } from "../theme"; + +// Marca "S" + wordmark a la izquierda; tema e idioma a la derecha. +export function AppBar({ children }: { children?: React.ReactNode }) { + return ( +
    + + S + sternboard + +
    + {children} + + +
    +
    + ); +} diff --git a/web/src/components/Drawer.tsx b/web/src/components/Drawer.tsx new file mode 100644 index 0000000..9f1cb5b --- /dev/null +++ b/web/src/components/Drawer.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; + +export function Drawer({ + title, + onClose, + children, +}: { + title: string; + onClose: () => void; + children: ReactNode; +}) { + return ( +
    + +
    + ); +} diff --git a/web/src/components/ExportModal.tsx b/web/src/components/ExportModal.tsx new file mode 100644 index 0000000..24cbeb3 --- /dev/null +++ b/web/src/components/ExportModal.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from "react"; +import { useI18n } from "../i18n"; + +type Opts = { + created: boolean; + exported: boolean; + groups: boolean; + authors: boolean; + votes: boolean; +}; + +export function ExportModal({ boardId, onClose }: { boardId: string; onClose: () => void }) { + const { t } = useI18n(); + const [opts, setOpts] = useState({ + created: true, + exported: false, + groups: false, + authors: true, + votes: true, + }); + const [preview, setPreview] = useState(""); + const [copied, setCopied] = useState(false); + + const query = (download = false) => { + const p = new URLSearchParams({ + created: opts.created ? "1" : "0", + exported: opts.exported ? "1" : "0", + groups: opts.groups ? "1" : "0", + authors: opts.authors ? "1" : "0", + votes: opts.votes ? "1" : "0", + }); + if (download) p.set("download", "1"); + return `/api/boards/${boardId}/export?${p.toString()}`; + }; + + useEffect(() => { + let live = true; + fetch(query()) + .then((r) => r.text()) + .then((txt) => live && setPreview(txt)) + .catch(() => {}); + return () => { + live = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [opts]); + + const toggle = (k: keyof Opts) => setOpts((o) => ({ ...o, [k]: !o[k] })); + const Row = ({ k, label }: { k: keyof Opts; label: string }) => ( + + ); + + async function copy() { + await navigator.clipboard.writeText(preview); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } + + return ( +
    +
    e.stopPropagation()}> +
    +

    {t("export.title")}

    + +
    +
    +
    +

    {t("export.options")}

    + + + + + +
    +
    +

    {t("export.preview")}

    +