From fa7864022757f8f521d8e001bd842aafae8ff5be Mon Sep 17 00:00:00 2001 From: jenstandstad Date: Fri, 14 Aug 2026 12:43:11 +0200 Subject: [PATCH] Initial commit --- Dockerfile | 18 + README.md | 89 + deploy.sh | 100 + docker-compose.dev.yml | 40 + docker-compose.prod.yml | 24 + docs/TODO.md | 26 + docs/exhibit-data-model.md | 330 ++ index.html | 13 + migrations/001_create_cases.sql | 9 + migrations/002_widgetize_levels.sql | 130 + migrations/003_document_assets.sql | 17 + .../004_folders_and_publication_time.sql | 56 + migrations/005_widget_runtime_state.sql | 15 + package-lock.json | 3990 +++++++++++++++++ package.json | 43 + server/index.ts | 290 ++ server/migrate.ts | 51 + src/App.tsx | 457 ++ src/main.tsx | 6 + src/styles.css | 177 + src/types.ts | 72 + tsconfig.app.json | 20 + tsconfig.json | 7 + tsconfig.node.json | 13 + vite.config.ts | 10 + 25 files changed, 6003 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 deploy.sh create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 docs/TODO.md create mode 100644 docs/exhibit-data-model.md create mode 100644 index.html create mode 100644 migrations/001_create_cases.sql create mode 100644 migrations/002_widgetize_levels.sql create mode 100644 migrations/003_document_assets.sql create mode 100644 migrations/004_folders_and_publication_time.sql create mode 100644 migrations/005_widget_runtime_state.sql create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/index.ts create mode 100644 server/migrate.ts create mode 100644 src/App.tsx create mode 100644 src/main.tsx create mode 100644 src/styles.css create mode 100644 src/types.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5cdf7c2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-bookworm-slim AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:22-bookworm-slim +ENV NODE_ENV=production PORT=8787 +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist +COPY --from=build /app/server ./server +COPY --from=build /app/src/types.ts ./src/types.ts +COPY --from=build /app/migrations ./migrations +EXPOSE 8787 +CMD ["node", "--import", "tsx", "server/index.ts"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8677aa --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# GUPI OSINT Board + +A standalone, server-backed proof of concept for the Glitch University investigation desktop. Levels are authored data: the repository contains the engine and schema, not a compiled-in case. + +The accepted normalized domain model and terminology are specified in [`docs/exhibit-data-model.md`](docs/exhibit-data-model.md). PostgreSQL stores **exhibits**; frontend **widgets** visualize exhibit types. + +Planned schema and exhibit work is tracked in [`docs/TODO.md`](docs/TODO.md). + +## Run locally + +Requires Node 20 or 22 (Node 22 is used by the production container). + +The self-contained development stack starts PostgreSQL, runs migrations, and serves the production build: + +```bash +docker compose -f docker-compose.dev.yml up --build +``` + +Open `http://localhost:8787`. PostgreSQL is also exposed on `localhost:5433` for inspection. + +For Vite hot reload, start only the database, then run the server and web app natively: + +```bash +docker compose -f docker-compose.dev.yml up -d db +cp .env.example .env +npm install +npm run migrate:up +npm run dev +``` + +Open `http://localhost:5173`; Vite proxies `/api` to port 8787. + +## Data and API + +The server stores its tables and migration ledger in the dedicated `osint` schema of PostgreSQL. `DATABASE_URL` is required. + +The accepted model stores each playable or editable level as an isolated mutable board of normalized exhibits. Immutable template-version boards are cloned to create levels, and mutable levels can be cloned back into new template versions. Assets remain immutable and reusable across those copies. + +The running POC still uses the earlier `widgets` and `playthrough_*` tables while the exhibit-schema cutover is prepared. They are explicitly transitional; new domain concepts should follow the exhibit model rather than extending those tables. + +The API surface is: + +- `GET /api/levels` +- `POST /api/levels` (editor only) +- `GET /api/levels/:id` +- `PUT /api/levels/:id` +- `POST /api/levels/:id/reset` +- `GET /api/health` + +The browser also keeps a local emergency copy so a network interruption does not lose an in-progress board. + +## Level editor + +Set `LEVEL_EDITING_ENABLED=true` and open `/?edit=1`. If the database is empty, this surface creates the first blank level. The target model intentionally makes editing and playing the same operation against a mutable level; authoring additionally exposes “save as template”. The current playthrough overlay remains only until the exhibit-schema migration is complete. + +In edit mode, files can be dragged from the desktop onto the board or selected with **Import Document**. Images, PDFs, and text files render inside document windows; unknown formats remain downloadable source files. Extracted evidence becomes an editable folder widget. Its editor controls the title, annotation, contained documents, and each source document's publication time. The default upload limit is 25 MB and can be changed with `MAX_DOCUMENT_BYTES`. + +Production defaults editing to disabled. Set `LEVEL_EDITING_ENABLED=true` in `/opt/gu_common/.env.prod` only when the authoring surface should be available. This is a capability switch, not authentication; add authentication before exposing production editing to untrusted users. + +## POC exhibit and widget contract + +The current POC defines four exhibit families and corresponding frontend widgets: + +- **Folder** — a titled, annotated collection. Opening it expands its contained source files to their saved relation-bound board positions; closing it retracts and hides them. +- **Source file** — immutable binary evidence plus user-editable title, publication time, file type, and arbitrary key/value metadata. Image is the first fully visual file renderer. PDF, web capture, email, article, filing, price list, text, and generic file are registered types with a generic fallback until their renderers are built. +- **Note** — investigator-authored interpretation, visually distinct from source evidence. +- **Event** — an investigator-authored “this happened” statement with narrative text, an occurrence time, and normalized links to its supporting evidence. Chronologically ordered events become the emerging story. + +The planned **Party** family adds distinct Person and Organization exhibits (businesses are organizations). Their dossier-like widgets reveal normalized evidence associations, but parties remain identity objects rather than special folders. + +Folder ownership is stored as a normalized membership. The contained document exhibit owns its expanded `xpos` and `ypos`. For deterministic collapse behavior, one exhibit has at most one owning folder; two exhibits may reuse the same immutable asset when the same source file must appear in multiple folders. + +An open folder draws a pale red containment band to each expanded file. Each dated file independently projects a grey line to the temporal index. This allows the player to arrange files until those grey lines are vertical, close the folder, and later reopen the same arrangement. + +The frontend widget registry maps an exhibit type, and optionally a document type, to its React visualization. Widgets do not own investigation-domain data. + +## Deploy at osint.glitch.university + +The production service depends on `gu_common`: it joins the external `gnommo` Docker network, uses `gnommo-db`, and is routed by the shared nginx container. Deploy `gu_common` after its nginx configuration changes, then deploy this repository: + +```bash +./deploy.sh +``` + +The deploy script builds and syncs the application, reads production database credentials from `/opt/gu_common/.env.prod`, executes pending migrations in the `osint` schema, starts `gnommo-osint-board`, and waits for its health endpoint. + +## Deliberate POC boundaries + +There are no accounts, arbitrary uploads, real-world web browsing, OCR, or collaboration yet. The server data model and provenance fields leave room for those later without making them part of the first playability test. diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..a4508ba --- /dev/null +++ b/deploy.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# deploy.sh - Deploy OSINT Board to the shared gu_common infrastructure + +set -e + +SKIP_PULL=false +if [ "$1" = "--skip-pull" ]; then + SKIP_PULL=true +fi + +SERVER="${DEPLOY_SERVER:-root@76.13.144.52}" +REMOTE_DIR="${DEPLOY_DIR:-/opt/osint-board}" +COMPOSE="docker compose -f ${REMOTE_DIR}/docker-compose.prod.yml --env-file /opt/gu_common/.env.prod" + +TARGET_HOST=$(echo "${SERVER}" | sed 's/.*@//') +OWN_IP=$(curl -sf --max-time 3 ifconfig.me 2>/dev/null || echo "unknown") +if [ "$OWN_IP" = "$TARGET_HOST" ]; then + echo "Error: deploy.sh must be run from your local machine, not the server." + exit 1 +fi + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +echo "==> Deploying OSINT Board to ${SERVER}:${REMOTE_DIR}" + +if [ "$SKIP_PULL" = true ]; then + echo "==> Skipping git pull (--skip-pull flag set)" +else + echo "==> Checking for upstream changes..." + HEAD_BEFORE=$(git -C "$REPO_DIR" rev-parse HEAD) + if ! git -C "$REPO_DIR" pull --ff-only; then + echo "Error: git pull failed. Resolve local state before deploying." + exit 1 + fi + HEAD_AFTER=$(git -C "$REPO_DIR" rev-parse HEAD) + if [ "$HEAD_BEFORE" != "$HEAD_AFTER" ]; then + echo "New upstream commits were pulled. Review and test them before deploying." + git -C "$REPO_DIR" log --oneline "${HEAD_BEFORE}..${HEAD_AFTER}" + exit 1 + fi +fi + +echo "==> Building locally..." +npm run build + +echo "==> Syncing files..." +ssh "$SERVER" "mkdir -p ${REMOTE_DIR}" +rsync -avz --delete \ + --exclude 'node_modules' \ + --exclude '.git' \ + --exclude 'dist' \ + --exclude '.env' \ + --exclude '.env.local' \ + --exclude '.env.prod' \ + --exclude '.DS_Store' \ + ./ "${SERVER}:${REMOTE_DIR}/" + +echo "==> Verifying gu_common configuration..." +ssh "$SERVER" "test -f /opt/gu_common/.env.prod || { echo 'ERROR: /opt/gu_common/.env.prod is missing'; exit 1; }" + +echo "==> Ensuring shared network exists..." +ssh "$SERVER" "docker network create gnommo 2>/dev/null || true" + +echo "==> Building application image..." +ssh "$SERVER" "CACHEBUST=\$(date +%s) && $COMPOSE build --build-arg CACHEBUST=\$CACHEBUST app" + +echo "==> Waiting for gu_common PostgreSQL..." +for i in $(seq 1 12); do + if ssh "$SERVER" "docker exec gnommo-db pg_isready -q" 2>/dev/null; then + echo " PostgreSQL is ready." + break + fi + echo " Not ready yet, waiting... ($i/12)" + sleep 5 + if [ "$i" -eq 12 ]; then + echo "ERROR: gu_common PostgreSQL did not become ready." + exit 1 + fi +done + +echo "==> Running osint schema migrations..." +ssh "$SERVER" "$COMPOSE run --rm --no-deps app npm run migrate:up" + +echo "==> Starting application..." +ssh "$SERVER" "$COMPOSE up -d app" + +echo "==> Waiting for OSINT Board health check..." +for i in $(seq 1 24); do + if ssh "$SERVER" "docker exec gnommo-osint-board node -e \"fetch('http://127.0.0.1:8787/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"" 2>/dev/null; then + echo " OSINT Board is healthy." + break + fi + echo " Not ready yet, waiting... ($i/24)" + sleep 5 + if [ "$i" -eq 24 ]; then + echo "ERROR: OSINT Board did not become healthy within 2 minutes." + exit 1 + fi +done + +echo "==> Done! https://osint.glitch.university" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..88e1865 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,40 @@ +services: + db: + image: postgres:16-alpine + container_name: osint-board-db + restart: unless-stopped + environment: + POSTGRES_DB: osint_dev + POSTGRES_USER: osint + POSTGRES_PASSWORD: osint_secret + ports: + - "5433:5432" + volumes: + - osint_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U osint -d osint_dev"] + interval: 3s + timeout: 3s + retries: 10 + + app: + build: + context: . + dockerfile: Dockerfile + container_name: osint-board-app + environment: + NODE_ENV: production + PORT: 8787 + DATABASE_URL: postgres://osint:osint_secret@db:5432/osint_dev + CORS_ORIGIN: http://localhost:8787 + LEVEL_EDITING_ENABLED: "true" + MAX_DOCUMENT_BYTES: 26214400 + ports: + - "8787:8787" + depends_on: + db: + condition: service_healthy + command: ["sh", "-c", "npm run migrate:up && npm start"] + +volumes: + osint_postgres_data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c2bced2 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,24 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: gnommo-osint-board + restart: unless-stopped + environment: + NODE_ENV: production + PORT: 8787 + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@gnommo-db:5432/${POSTGRES_DB} + CORS_ORIGIN: https://osint.${DOMAIN} + LEVEL_EDITING_ENABLED: ${LEVEL_EDITING_ENABLED:-false} + MAX_DOCUMENT_BYTES: ${MAX_DOCUMENT_BYTES:-26214400} + expose: + - "8787" + networks: + - default + - gnommo + +networks: + default: {} + gnommo: + external: true diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..0303222 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,26 @@ +# GUPI OSINT Board roadmap + +This list tracks domain and product work that follows the accepted exhibit model. It is not a substitute for migrations or implementation issues. + +## Exhibit-schema cutover + +- [ ] Add boards, exhibits, exhibit types, subtype tables, immutable template versions, and cloned mutable levels. +- [ ] Migrate transitional `widgets`, `widget_relations`, and `playthrough_*` data with equivalence checks. +- [ ] Implement template instantiation and “save level as template” as transactional clone operations. +- [ ] Move the frontend to an exhibit/widget registry backed by the normalized API. + +## Party exhibits + +- [ ] Add a Party exhibit supertype representing an investigation participant. +- [ ] Add a Person subtype with display name, normalized aliases, and extensible structured identity fields. +- [ ] Add an Organization subtype covering businesses, public bodies, associations, and informal groups. +- [ ] Add normalized many-to-many Party-to-Evidence associations with an optional explanatory note. +- [ ] Add typed Party-to-Party relationships such as employment, ownership, membership, control, and representation. +- [ ] Build distinct Person and Organization widgets that can open as dossiers and reveal associated evidence without using folder ownership semantics. +- [ ] Include parties, aliases, evidence associations, and party relationships in template/level cloning. + +## Events and narrative + +- [ ] Implement Event exhibits with occurrence time and investigator-authored narrative text. +- [ ] Implement normalized Event-to-Evidence links and their board visualization. +- [ ] Present chronologically ordered events as the emerging investigation story. diff --git a/docs/exhibit-data-model.md b/docs/exhibit-data-model.md new file mode 100644 index 0000000..2eca99d --- /dev/null +++ b/docs/exhibit-data-model.md @@ -0,0 +1,330 @@ +# GUPI OSINT Board: canonical exhibit data model + +Status: accepted design foundation. + +## Vocabulary + +- An **exhibit** is a durable investigation-domain object stored in PostgreSQL. +- An **exhibit type** describes its domain behavior: folder, document, clipping, note, event, party, or conclusion. +- A **widget** is the frontend visualization and interaction implementation selected for an exhibit type. +- A **document type** specializes a document exhibit: image, PDF, web capture, email, article, filing, price list, text, or generic file. +- A **board** is a neutral container for exhibits. Both mutable levels and immutable template versions own boards. +- A **level** is a mutable board copy used for either play or authoring. +- A **level template version** is an immutable board snapshot. + +The frontend registry maps `exhibit_type` (and, for documents, `document_type`) to a widget component. Changing a widget must never change the meaning or storage of its exhibit. + +## Lifecycle + +Templates are never edited in place. + +1. A template family has one or more immutable versions. +2. Starting work creates a new level and clones the selected template-version board into it. +3. Every cloned exhibit receives a new ID, belongs to the new level board, and retains provenance through `origin_exhibit_id`. +4. Playing and editing write to exactly the same level and exhibit tables. +5. “Save as template” clones the current level board into a new immutable template version. +6. Binary assets are immutable and shared; document exhibits are copied, asset bytes are not. + +No long-term `playthrough_*` overlay is required. A level is already the user's isolated working copy. + +## Core structure + +```sql +CREATE TABLE osint.boards ( + id UUID PRIMARY KEY, + board_kind TEXT NOT NULL CHECK (board_kind IN ('level', 'template_version')), + revision BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE osint.level_templates ( + id UUID PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + current_version_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE osint.level_template_versions ( + id UUID PRIMARY KEY, + template_id UUID NOT NULL REFERENCES osint.level_templates(id), + version INTEGER NOT NULL, + board_id UUID NOT NULL UNIQUE REFERENCES osint.boards(id), + title TEXT NOT NULL, + subtitle TEXT NOT NULL DEFAULT '', + created_from_level_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (template_id, version) +); + +CREATE TABLE osint.levels ( + id UUID PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + board_id UUID NOT NULL UNIQUE REFERENCES osint.boards(id), + source_template_version_id UUID REFERENCES osint.level_template_versions(id), + title TEXT NOT NULL, + subtitle TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL, + viewport_x DOUBLE PRECISION NOT NULL DEFAULT 0, + viewport_y DOUBLE PRECISION NOT NULL DEFAULT 28, + viewport_zoom DOUBLE PRECISION NOT NULL DEFAULT 0.7, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +`boards.revision` is incremented transactionally and used for optimistic concurrency control. + +## Exhibits + +Common spatial and lifecycle properties belong to the base exhibit, not to its widget or subtype. + +```sql +CREATE TABLE osint.exhibit_types ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + is_spatial BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE osint.exhibits ( + id UUID PRIMARY KEY, + board_id UUID NOT NULL REFERENCES osint.boards(id) ON DELETE CASCADE, + exhibit_type_id TEXT NOT NULL REFERENCES osint.exhibit_types(id), + origin_exhibit_id UUID REFERENCES osint.exhibits(id) ON DELETE SET NULL, + xpos DOUBLE PRECISION NOT NULL, + ypos DOUBLE PRECISION NOT NULL, + width DOUBLE PRECISION NOT NULL, + height DOUBLE PRECISION NOT NULL, + rotation DOUBLE PRECISION NOT NULL DEFAULT 0, + z_index INTEGER NOT NULL DEFAULT 0, + hidden BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (board_id, id) +); +``` + +The common table is extended with one-to-one subtype tables. Domain fields do not live in an unstructured `config` document. + +```sql +CREATE TABLE osint.folder_exhibits ( + exhibit_id UUID PRIMARY KEY REFERENCES osint.exhibits(id) ON DELETE CASCADE, + title TEXT NOT NULL, + label_text TEXT NOT NULL DEFAULT '', + is_open BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE osint.document_types ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE osint.document_exhibits ( + exhibit_id UUID PRIMARY KEY REFERENCES osint.exhibits(id) ON DELETE CASCADE, + document_type_id TEXT NOT NULL REFERENCES osint.document_types(id), + asset_id UUID REFERENCES osint.assets(id), + title TEXT NOT NULL, + published_at TIMESTAMPTZ, + captured_at TIMESTAMPTZ, + source_uri TEXT +); + +CREATE TABLE osint.image_documents ( + exhibit_id UUID PRIMARY KEY REFERENCES osint.document_exhibits(exhibit_id) ON DELETE CASCADE, + pixel_width INTEGER CHECK (pixel_width > 0), + pixel_height INTEGER CHECK (pixel_height > 0), + alt_text TEXT NOT NULL DEFAULT '' +); + +CREATE TABLE osint.note_exhibits ( + exhibit_id UUID PRIMARY KEY REFERENCES osint.exhibits(id) ON DELETE CASCADE, + title TEXT NOT NULL, + note_text TEXT NOT NULL +); + +CREATE TABLE osint.event_exhibits ( + exhibit_id UUID PRIMARY KEY REFERENCES osint.exhibits(id) ON DELETE CASCADE, + title TEXT NOT NULL, + narrative_text TEXT NOT NULL, + occurred_at TIMESTAMPTZ NOT NULL +); +``` + +The service validates that every base exhibit has exactly one subtype row matching `exhibit_type_id`. + +### Event semantics + +An event is an investigator-authored assertion: **“this happened.”** It is not source evidence and must not silently inherit a document's publication time. + +- `narrative_text` states what the investigator believes happened. +- `occurred_at` places that assertion in reconstructed time. +- One event may cite several supporting exhibits. +- One exhibit may support several events. +- Events ordered by `occurred_at` form the emerging case narrative; there is no duplicated story-text record. + +Supporting evidence is an explicit normalized relationship: + +```sql +CREATE TABLE osint.event_evidence ( + event_exhibit_id UUID NOT NULL REFERENCES osint.event_exhibits(exhibit_id) ON DELETE CASCADE, + evidence_exhibit_id UUID NOT NULL REFERENCES osint.exhibits(id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL DEFAULT 0, + note TEXT, + PRIMARY KEY (event_exhibit_id, evidence_exhibit_id), + CHECK (event_exhibit_id <> evidence_exhibit_id) +); +``` + +A deferred constraint trigger verifies that both exhibits belong to the same board and that the evidence endpoint is not itself the same event. The optional `note` explains why that exhibit supports the event; it does not replace the event's narrative. + +In the frontend, the Event widget shows its occurrence time, narrative text, and evidence count. Opening or selecting it reveals its supporting exhibits. Lines between an event and its evidence visualize `event_evidence`; they are not ordinary folder containment bands. + +### Party semantics (planned) + +A party is a person or organization that participates in the investigation. Businesses are organizations. A party is a first-class exhibit, not a special folder: identity and evidence association must not be represented as file ownership. + +- A **person** has a display name and may have aliases and other structured identity fields. +- An **organization** has a display name and an organization kind such as business, public body, association, or informal group. +- A party may be linked to several supporting exhibits, and one exhibit may concern several parties. +- Relationships between parties—employment, ownership, membership, control, representation, or an investigator-defined connection—are explicit typed relationships rather than containment. + +The intended normalized shape is a `party_exhibits` supertype with one-to-one `person_parties` and `organization_parties` subtype tables, plus a many-to-many `party_evidence` association. Aliases and party-to-party relationships use child tables rather than arrays or widget configuration JSON. + +The frontend provides distinct Person and Organization widgets through the exhibit registry. They may visually behave like dossiers—opening one can reveal associated evidence—but that interaction is derived from `party_evidence`; it does not turn the party into a folder or cause evidence to be owned by or disappear into the party. + +## Assets and document content + +`assets` stores immutable uploaded bytes, checksum, MIME type, original filename, and size. Multiple cloned document exhibits may reference one asset. + +Document text and extractable regions are normalized separately: + +```sql +CREATE TABLE osint.document_content_blocks ( + id UUID PRIMARY KEY, + document_exhibit_id UUID NOT NULL REFERENCES osint.document_exhibits(exhibit_id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL, + content TEXT NOT NULL, + UNIQUE (document_exhibit_id, sort_order) +); + +CREATE TABLE osint.document_regions ( + id UUID PRIMARY KEY, + document_exhibit_id UUID NOT NULL REFERENCES osint.document_exhibits(exhibit_id) ON DELETE CASCADE, + region_key TEXT NOT NULL, + label TEXT NOT NULL, + excerpt TEXT NOT NULL, + occurred_at TIMESTAMPTZ, + sort_order INTEGER NOT NULL DEFAULT 0, + UNIQUE (document_exhibit_id, region_key) +); +``` + +## Folder ownership + +Folder containment is a domain relationship, not a generic visual connection. + +```sql +CREATE TABLE osint.folder_memberships ( + board_id UUID NOT NULL REFERENCES osint.boards(id) ON DELETE CASCADE, + folder_exhibit_id UUID NOT NULL REFERENCES osint.folder_exhibits(exhibit_id) ON DELETE CASCADE, + child_exhibit_id UUID NOT NULL REFERENCES osint.exhibits(id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (folder_exhibit_id, child_exhibit_id), + UNIQUE (board_id, child_exhibit_id), + CHECK (folder_exhibit_id <> child_exhibit_id) +); +``` + +For the POC, one exhibit has at most one owning folder. This makes open/close behavior deterministic. If the same uploaded image must appear in two folders, two document exhibits reference the same immutable asset. + +Composite foreign keys or deferred constraint triggers enforce that folder, child, and membership all belong to the same board and that folders cannot contain themselves or form containment cycles. + +The pale red folder band is derived from this table. Its geometry is not stored. + +## Investigative connections and provenance + +```sql +CREATE TABLE osint.connection_types ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + directed BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE osint.exhibit_connections ( + id UUID PRIMARY KEY, + board_id UUID NOT NULL REFERENCES osint.boards(id) ON DELETE CASCADE, + connection_type_id TEXT NOT NULL REFERENCES osint.connection_types(id), + from_exhibit_id UUID NOT NULL REFERENCES osint.exhibits(id) ON DELETE CASCADE, + to_exhibit_id UUID NOT NULL REFERENCES osint.exhibits(id) ON DELETE CASCADE, + label TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CHECK (from_exhibit_id <> to_exhibit_id) +); + +CREATE TABLE osint.exhibit_sources ( + exhibit_id UUID PRIMARY KEY REFERENCES osint.exhibits(id) ON DELETE CASCADE, + source_document_exhibit_id UUID NOT NULL REFERENCES osint.document_exhibits(exhibit_id), + source_region_id UUID REFERENCES osint.document_regions(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +Red investigative thread is derived from `exhibit_connections`. Extraction provenance is stored in `exhibit_sources`; it is not represented as a visual thread. + +## Document metadata + +Known, semantically important values remain real columns: `published_at`, `captured_at`, and `source_uri`. Truly author-defined fields use typed metadata definitions and values rather than JSONB or one untyped EAV table. + +```sql +CREATE TABLE osint.metadata_fields ( + id UUID PRIMARY KEY, + key TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + value_type TEXT NOT NULL CHECK (value_type IN ('text', 'timestamp', 'number', 'boolean')) +); + +CREATE TABLE osint.document_metadata_text_values ( + document_exhibit_id UUID NOT NULL REFERENCES osint.document_exhibits(exhibit_id) ON DELETE CASCADE, + field_id UUID NOT NULL REFERENCES osint.metadata_fields(id), + value TEXT NOT NULL, + PRIMARY KEY (document_exhibit_id, field_id) +); +``` + +Timestamp, number, and boolean values use equivalent type-specific tables. This lets PostgreSQL enforce actual value types and avoids nullable multi-type value columns. + +## Derived presentation + +The following are computed and must not become duplicate source-of-truth tables: + +- Grey timeline projection: document exhibit position to `published_at` on the timeline. +- Pale red containment band: folder position to contained exhibit position. +- Folder flight animation: closed folder position to the exhibit's stored `xpos` and `ypos`. +- Widget selection: `exhibit_type` plus optional `document_type` mapped through the frontend registry. + +## Transactional operations + +`instantiate_template_version(source_version_id)` performs one database transaction: + +1. Create the level board and level row. +2. Clone all exhibits and build an old-to-new ID map. +3. Clone each subtype row. +4. Clone content, typed metadata, and provenance through the map. +5. Clone folder memberships, event-evidence links, and exhibit connections through the map. +6. Reuse asset IDs. +7. Commit only after all constraints pass. + +`save_level_as_template(level_id)` runs the same clone operation into a new immutable template-version board. + +## Migration direction + +The current `widgets`, `widget_relations`, and `playthrough_*` tables are transitional. The cutover should: + +1. Introduce boards, exhibit tables, subtype tables, templates, and typed metadata. +2. Convert existing authored widgets to exhibits. +3. Materialize each existing playthrough as its own cloned mutable level. +4. Move document layout from relation JSON into document exhibit coordinates. +5. Convert `contains` relations to folder memberships and generic connections to exhibit connections. +6. Update the API to read and write exhibits directly. +7. Remove the transitional widget/playthrough tables only after data equivalence checks pass. diff --git a/index.html b/index.html new file mode 100644 index 0000000..f40471a --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + GUPI OSINT Board + + +
+ + + diff --git a/migrations/001_create_cases.sql b/migrations/001_create_cases.sql new file mode 100644 index 0000000..f8eccd5 --- /dev/null +++ b/migrations/001_create_cases.sql @@ -0,0 +1,9 @@ +CREATE TABLE osint.cases ( + id TEXT PRIMARY KEY, + state_json JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON SCHEMA osint IS 'GUPI OSINT Board application data'; +COMMENT ON TABLE osint.cases IS 'Server-authoritative investigation board state'; diff --git a/migrations/002_widgetize_levels.sql b/migrations/002_widgetize_levels.sql new file mode 100644 index 0000000..c2c3522 --- /dev/null +++ b/migrations/002_widgetize_levels.sql @@ -0,0 +1,130 @@ +CREATE TABLE osint.levels ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + subtitle TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE osint.widgets ( + id TEXT PRIMARY KEY, + level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE, + widget_type TEXT NOT NULL CHECK (widget_type IN ('document', 'evidence', 'note', 'event')), + title TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + config JSONB NOT NULL DEFAULT '{}'::jsonb, + source_widget_id TEXT REFERENCES osint.widgets(id) ON DELETE SET NULL, + source_region_key TEXT, + event_date DATE, + x DOUBLE PRECISION, + y DOUBLE PRECISION, + width DOUBLE PRECISION, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (level_id, id) +); + +CREATE INDEX widgets_level_type_idx ON osint.widgets(level_id, widget_type, sort_order); + +CREATE TABLE osint.widget_regions ( + id TEXT PRIMARY KEY, + document_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE, + region_key TEXT NOT NULL, + label TEXT NOT NULL, + excerpt TEXT NOT NULL, + event_date DATE, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + sort_order INTEGER NOT NULL DEFAULT 0, + UNIQUE (document_widget_id, region_key) +); + +CREATE TABLE osint.level_connections ( + id TEXT PRIMARY KEY, + level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE, + from_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE, + to_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE, + config JSONB NOT NULL DEFAULT '{}'::jsonb +); + +CREATE TABLE osint.playthroughs ( + id TEXT PRIMARY KEY, + level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'completed', 'abandoned')), + viewport JSONB NOT NULL DEFAULT '{"x":0,"y":28,"zoom":0.7}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE osint.playthrough_widget_state ( + playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE, + widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE, + x DOUBLE PRECISION NOT NULL, + y DOUBLE PRECISION NOT NULL, + width DOUBLE PRECISION NOT NULL, + hidden BOOLEAN NOT NULL DEFAULT FALSE, + annotation TEXT, + PRIMARY KEY (playthrough_id, widget_id) +); + +CREATE TABLE osint.playthrough_widgets ( + id TEXT PRIMARY KEY, + playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE, + widget_type TEXT NOT NULL CHECK (widget_type IN ('evidence', 'note', 'event')), + title TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + source_widget_id TEXT REFERENCES osint.widgets(id) ON DELETE SET NULL, + source_region_key TEXT, + event_date DATE, + x DOUBLE PRECISION NOT NULL, + y DOUBLE PRECISION NOT NULL, + width DOUBLE PRECISION NOT NULL, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE osint.playthrough_connections ( + id TEXT PRIMARY KEY, + playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE, + from_widget_id TEXT NOT NULL, + to_widget_id TEXT NOT NULL, + config JSONB NOT NULL DEFAULT '{}'::jsonb +); + +-- Preserve levels saved by the pre-widget architecture, if any exist. +INSERT INTO osint.levels (id, title, subtitle, status, created_at, updated_at) +SELECT id, state_json->>'title', COALESCE(state_json->>'subtitle', ''), 'draft', created_at, updated_at +FROM osint.cases; + +INSERT INTO osint.widgets (id, level_id, widget_type, title, content, config, event_date, sort_order) +SELECT doc->>'id', c.id, 'document', doc->>'title', '', + jsonb_build_object('kind', doc->>'kind', 'date', doc->>'date', 'body', COALESCE(doc->'body', '[]'::jsonb)), + NULLIF(doc->>'date', '')::date, ordinality::integer +FROM osint.cases c +CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'documents') WITH ORDINALITY AS item(doc, ordinality); + +INSERT INTO osint.widget_regions (id, document_widget_id, region_key, label, excerpt, event_date, sort_order) +SELECT (doc->>'id') || ':' || (region->>'id'), doc->>'id', region->>'id', region->>'label', region->>'excerpt', + NULLIF(region->>'date', '')::date, region_ordinality::integer +FROM osint.cases c +CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'documents') AS docs(doc) +CROSS JOIN LATERAL jsonb_array_elements(doc->'regions') WITH ORDINALITY AS regions(region, region_ordinality); + +INSERT INTO osint.widgets (id, level_id, widget_type, title, content, source_widget_id, source_region_key, event_date, x, y, width, sort_order) +SELECT ev->>'id', c.id, ev->>'type', ev->>'title', ev->>'content', NULLIF(ev->>'sourceDocumentId', ''), + NULLIF(ev->>'sourceRegionId', ''), NULLIF(ev->>'eventDate', '')::date, + (ev->>'x')::double precision, (ev->>'y')::double precision, (ev->>'width')::double precision, ordinality::integer +FROM osint.cases c +CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'evidence') WITH ORDINALITY AS item(ev, ordinality); + +INSERT INTO osint.level_connections (id, level_id, from_widget_id, to_widget_id) +SELECT conn->>'id', c.id, conn->>'fromEvidenceId', conn->>'toEvidenceId' +FROM osint.cases c +CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'connections') AS connections(conn); + +INSERT INTO osint.playthroughs (id, level_id, viewport) +SELECT 'default:' || id, id, COALESCE(state_json->'viewport', '{"x":0,"y":28,"zoom":0.7}'::jsonb) +FROM osint.cases; + +DROP TABLE osint.cases; diff --git a/migrations/003_document_assets.sql b/migrations/003_document_assets.sql new file mode 100644 index 0000000..a596c6d --- /dev/null +++ b/migrations/003_document_assets.sql @@ -0,0 +1,17 @@ +CREATE TABLE osint.assets ( + id TEXT PRIMARY KEY, + level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE, + original_name TEXT NOT NULL, + mime_type TEXT NOT NULL, + byte_size BIGINT NOT NULL CHECK (byte_size >= 0), + content BYTEA NOT NULL, + checksum_sha256 TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX assets_level_idx ON osint.assets(level_id, created_at); + +ALTER TABLE osint.widgets + ADD COLUMN asset_id TEXT REFERENCES osint.assets(id) ON DELETE SET NULL; + +COMMENT ON TABLE osint.assets IS 'Uploaded source files used by database-backed document widgets'; diff --git a/migrations/004_folders_and_publication_time.sql b/migrations/004_folders_and_publication_time.sql new file mode 100644 index 0000000..2717d79 --- /dev/null +++ b/migrations/004_folders_and_publication_time.sql @@ -0,0 +1,56 @@ +-- Widget kinds are intentionally data-driven. Removing enum-like checks means +-- future widget types do not require a table rewrite merely to exist. +ALTER TABLE osint.widgets DROP CONSTRAINT IF EXISTS widgets_widget_type_check; +ALTER TABLE osint.playthrough_widgets DROP CONSTRAINT IF EXISTS playthrough_widgets_widget_type_check; + +ALTER TABLE osint.widgets ADD COLUMN published_at TIMESTAMPTZ; + +UPDATE osint.widgets +SET published_at = event_date::timestamp AT TIME ZONE 'UTC' +WHERE widget_type = 'document' AND event_date IS NOT NULL; + +CREATE TABLE osint.widget_relations ( + id TEXT PRIMARY KEY, + level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE, + from_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE, + to_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE, + relation_type TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (from_widget_id, to_widget_id, relation_type) +); + +CREATE INDEX widget_relations_from_idx ON osint.widget_relations(level_id, from_widget_id, relation_type); +CREATE INDEX widget_relations_to_idx ON osint.widget_relations(level_id, to_widget_id, relation_type); + +CREATE TABLE osint.playthrough_widget_relations ( + id TEXT PRIMARY KEY, + playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE, + from_widget_id TEXT NOT NULL, + to_widget_id TEXT NOT NULL, + relation_type TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + UNIQUE (playthrough_id, from_widget_id, to_widget_id, relation_type) +); + +UPDATE osint.widgets SET widget_type = 'folder' WHERE widget_type = 'evidence'; +UPDATE osint.playthrough_widgets SET widget_type = 'folder' WHERE widget_type = 'evidence'; + +INSERT INTO osint.widget_relations (id, level_id, from_widget_id, to_widget_id, relation_type) +SELECT 'contains:' || id || ':' || source_widget_id, level_id, id, source_widget_id, 'contains' +FROM osint.widgets +WHERE widget_type = 'folder' AND source_widget_id IS NOT NULL +ON CONFLICT (from_widget_id, to_widget_id, relation_type) DO NOTHING; + +INSERT INTO osint.playthrough_widget_relations + (id, playthrough_id, from_widget_id, to_widget_id, relation_type) +SELECT 'contains:' || playthrough_id || ':' || id || ':' || source_widget_id, + playthrough_id, id, source_widget_id, 'contains' +FROM osint.playthrough_widgets +WHERE widget_type = 'folder' AND source_widget_id IS NOT NULL +ON CONFLICT (playthrough_id, from_widget_id, to_widget_id, relation_type) DO NOTHING; + +COMMENT ON COLUMN osint.widgets.published_at IS 'Publication/creation time of a source document; chronology belongs to documents, not folders'; +COMMENT ON TABLE osint.widget_relations IS 'Generic authored widget graph; contains is the first relation type'; diff --git a/migrations/005_widget_runtime_state.sql b/migrations/005_widget_runtime_state.sql new file mode 100644 index 0000000..ec4b9f2 --- /dev/null +++ b/migrations/005_widget_runtime_state.sql @@ -0,0 +1,15 @@ +-- Runtime state is generic JSON so future widget and relation renderers can +-- persist interaction-specific state without adding a column per widget kind. +ALTER TABLE osint.playthrough_widget_state +ADD COLUMN config JSONB NOT NULL DEFAULT '{}'::jsonb; + +CREATE TABLE osint.playthrough_widget_relation_state ( + playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE, + relation_id TEXT NOT NULL REFERENCES osint.widget_relations(id) ON DELETE CASCADE, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (playthrough_id, relation_id) +); + +COMMENT ON COLUMN osint.playthrough_widget_state.config IS 'Player-owned runtime and metadata overrides for any authored widget type'; +COMMENT ON TABLE osint.playthrough_widget_relation_state IS 'Player-owned layout/state overrides for authored widget relations'; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f984399 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3990 @@ +{ + "name": "gupi-osint-board", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gupi-osint-board", + "version": "0.1.0", + "dependencies": { + "cors": "2.8.5", + "dotenv": "16.5.0", + "express": "5.1.0", + "lucide-react": "0.468.0", + "multer": "2.0.2", + "pg": "8.16.3", + "react": "19.1.0", + "react-dom": "19.1.0", + "tsx": "4.20.3" + }, + "devDependencies": { + "@types/cors": "2.8.18", + "@types/express": "5.0.3", + "@types/multer": "2.0.0", + "@types/node": "22.15.30", + "@types/pg": "8.15.4", + "@types/react": "19.1.8", + "@types/react-dom": "19.1.6", + "@vitejs/plugin-react": "4.5.2", + "concurrently": "9.1.2", + "typescript": "5.8.3", + "vite": "6.3.5", + "vitest": "3.2.3" + }, + "engines": { + "node": ">=20" + } + }, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "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/@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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", + "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz", + "integrity": "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "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/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz", + "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.11", + "@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-beta.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", + "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", + "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.3", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", + "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.3", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", + "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.3", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", + "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", + "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", + "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.3", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", + "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "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/baseline-browser-mapping": { + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "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/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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/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/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.406", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.406.tgz", + "integrity": "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "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/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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.2.tgz", + "integrity": "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "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": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "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==", + "dev": true, + "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/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "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": "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/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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/multer/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/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "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": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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/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/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": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "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": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "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/vite-node": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", + "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", + "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.3", + "@vitest/mocker": "3.2.3", + "@vitest/pretty-format": "^3.2.3", + "@vitest/runner": "3.2.3", + "@vitest/snapshot": "3.2.3", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.3", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.3", + "@vitest/ui": "3.2.3", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b10d9e --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "gupi-osint-board", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "concurrently -k \"npm:dev:server\" \"npm:dev:web\"", + "dev:web": "vite", + "dev:server": "tsx watch server/index.ts", + "build": "tsc -b && vite build", + "start": "NODE_ENV=production tsx server/index.ts", + "migrate:up": "tsx server/migrate.ts", + "test": "vitest run" + }, + "dependencies": { + "cors": "2.8.5", + "dotenv": "16.5.0", + "express": "5.1.0", + "lucide-react": "0.468.0", + "multer": "2.0.2", + "pg": "8.16.3", + "react": "19.1.0", + "react-dom": "19.1.0", + "tsx": "4.20.3" + }, + "devDependencies": { + "@types/cors": "2.8.18", + "@types/express": "5.0.3", + "@types/node": "22.15.30", + "@types/multer": "2.0.0", + "@types/pg": "8.15.4", + "@types/react": "19.1.8", + "@types/react-dom": "19.1.6", + "@vitejs/plugin-react": "4.5.2", + "concurrently": "9.1.2", + "typescript": "5.8.3", + "vite": "6.3.5", + "vitest": "3.2.3" + }, + "engines": { + "node": ">=20" + } +} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..0f9bec6 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,290 @@ +import 'dotenv/config' +import cors from 'cors' +import express from 'express' +import fs from 'node:fs' +import { createHash, randomUUID } from 'node:crypto' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import multer from 'multer' +import pg, { type PoolClient } from 'pg' +import type { CaseDocument, CaseState, Connection, Evidence, WidgetRelation } from '../src/types.js' + +const { Pool } = pg +const databaseUrl = process.env.DATABASE_URL +if (!databaseUrl) { + console.error('DATABASE_URL is required. Run PostgreSQL and execute npm run migrate:up first.') + process.exit(1) +} + +const pool = new Pool({ connectionString: databaseUrl }) +const editingEnabled = process.env.LEVEL_EDITING_ENABLED === 'true' + +type WidgetRow = { + id: string; widget_type: 'document' | Evidence['type']; title: string; content: string + config: { kind?: string; date?: string; body?: string[]; fileType?: CaseDocument['fileType']; metadata?: Record; [key: string]: unknown }; source_widget_id?: string; source_region_key?: string + event_date?: string; published_at?: string; x?: number; y?: number; width?: number; sort_order: number; asset_id?: string + original_name?: string; mime_type?: string; byte_size?: number +} + +function wantsEdit(req: express.Request) { + return editingEnabled && req.query.edit === '1' +} + +async function assembleLevel(levelId: string, playthroughId = `default:${levelId}`, authorMode = false): Promise { + const levelResult = await pool.query<{ id: string; title: string; subtitle: string; status: string }>( + 'SELECT id, title, subtitle, status FROM osint.levels WHERE id = $1', [levelId], + ) + const level = levelResult.rows[0] + if (!level) return null + + await pool.query( + `INSERT INTO osint.playthroughs (id, level_id) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`, + [playthroughId, levelId], + ) + const [widgetsResult, regionsResult, authoredConnections, authoredRelations, playthroughResult, generatedResult, playerConnections, playerRelations, stateResult, relationStateResult] = await Promise.all([ + pool.query(`SELECT w.id, w.widget_type, w.title, w.content, w.config, w.source_widget_id, w.source_region_key, + w.event_date::text, w.published_at::text, w.x, w.y, w.width, w.sort_order, w.asset_id, a.original_name, a.mime_type, a.byte_size + FROM osint.widgets w LEFT JOIN osint.assets a ON a.id = w.asset_id + WHERE w.level_id = $1 ORDER BY w.sort_order, w.id`, [levelId]), + pool.query<{ document_widget_id: string; region_key: string; label: string; excerpt: string; event_date?: string }>( + `SELECT r.document_widget_id, r.region_key, r.label, r.excerpt, r.event_date::text + FROM osint.widget_regions r JOIN osint.widgets w ON w.id = r.document_widget_id + WHERE w.level_id = $1 ORDER BY r.sort_order, r.id`, [levelId]), + pool.query<{ id: string; from_widget_id: string; to_widget_id: string }>( + 'SELECT id, from_widget_id, to_widget_id FROM osint.level_connections WHERE level_id = $1', [levelId]), + pool.query<{ id: string; from_widget_id: string; to_widget_id: string; relation_type: string; sort_order: number; config: Record }>( + `SELECT id, from_widget_id, to_widget_id, relation_type, sort_order, config FROM osint.widget_relations + WHERE level_id = $1 ORDER BY sort_order, id`, [levelId]), + pool.query<{ viewport: CaseState['viewport']; updated_at: Date }>( + 'SELECT viewport, updated_at FROM osint.playthroughs WHERE id = $1', [playthroughId]), + pool.query(`SELECT id, widget_type, title, content, config, source_widget_id, + source_region_key, event_date::text, x, y, width, 0 AS sort_order + FROM osint.playthrough_widgets WHERE playthrough_id = $1 ORDER BY created_at, id`, [playthroughId]), + pool.query<{ id: string; from_widget_id: string; to_widget_id: string }>( + 'SELECT id, from_widget_id, to_widget_id FROM osint.playthrough_connections WHERE playthrough_id = $1', [playthroughId]), + pool.query<{ id: string; from_widget_id: string; to_widget_id: string; relation_type: string; sort_order: number; config: Record }>( + `SELECT id, from_widget_id, to_widget_id, relation_type, sort_order, config FROM osint.playthrough_widget_relations + WHERE playthrough_id = $1 ORDER BY sort_order, id`, [playthroughId]), + pool.query<{ widget_id: string; x: number; y: number; width: number; hidden: boolean; config: Record }>( + 'SELECT widget_id, x, y, width, hidden, config FROM osint.playthrough_widget_state WHERE playthrough_id = $1', [playthroughId]), + pool.query<{ relation_id: string; config: Record }>( + 'SELECT relation_id, config FROM osint.playthrough_widget_relation_state WHERE playthrough_id = $1', [playthroughId]), + ]) + + const stateByWidget = new Map(authorMode ? [] : stateResult.rows.map(row => [row.widget_id, row])) + const documents: CaseDocument[] = widgetsResult.rows.filter(w => w.widget_type === 'document').map(w => { const override = stateByWidget.get(w.id)?.config || {}; const publishedAt = String(override.publishedAt || w.published_at || ''); return ({ + id: w.id, title: String(override.title || w.title), kind: w.config.kind || 'DOCUMENT', date: publishedAt.slice(0, 10) || w.config.date || '', publishedAt: publishedAt || undefined, + body: w.config.body || [], fileType: (override.fileType || w.config.fileType || (w.mime_type?.startsWith('image/') ? 'image' : 'file')) as CaseDocument['fileType'], metadata: (override.metadata || w.config.metadata || {}) as Record, + assetId: w.asset_id, fileName: w.original_name, mimeType: w.mime_type, fileSize: w.byte_size, + regions: regionsResult.rows.filter(r => r.document_widget_id === w.id).map(r => ({ id: r.region_key, label: r.label, excerpt: r.excerpt, date: r.event_date })), + }) }) + const relationState = new Map(authorMode ? [] : relationStateResult.rows.map(row => [row.relation_id, row.config])) + const containedByFolder = new Map() + for (const relation of [...authoredRelations.rows, ...(authorMode ? [] : playerRelations.rows)]) { + if (relation.relation_type !== 'contains') continue + containedByFolder.set(relation.from_widget_id, [...(containedByFolder.get(relation.from_widget_id) || []), relation.to_widget_id]) + } + const toEvidence = (w: WidgetRow): Evidence => { + const override = stateByWidget.get(w.id) + const runtimeConfig = { ...w.config, ...(override?.config || {}) } + return { id: w.id, type: w.widget_type as Evidence['type'], title: String(override?.config?.title || w.title), content: String(override?.config?.content ?? w.content), config: runtimeConfig, + sourceDocumentId: w.source_widget_id, sourceRegionId: w.source_region_key, eventDate: w.widget_type === 'event' ? w.event_date : undefined, + containedDocumentIds: containedByFolder.get(w.id) || (w.source_widget_id ? [w.source_widget_id] : []), + x: override?.x ?? w.x ?? 100, y: override?.y ?? w.y ?? 100, width: override?.width ?? w.width ?? 240 } + } + const authoredEvidence = widgetsResult.rows.filter(w => w.widget_type !== 'document' && !stateByWidget.get(w.id)?.hidden).map(toEvidence) + const evidence = [...authoredEvidence, ...(authorMode ? [] : generatedResult.rows.map(toEvidence))] + const connections: Connection[] = [...authoredConnections.rows, ...(authorMode ? [] : playerConnections.rows)].map(c => ({ + id: c.id, fromEvidenceId: c.from_widget_id, toEvidenceId: c.to_widget_id, + })) + const relations: WidgetRelation[] = [...authoredRelations.rows, ...(authorMode ? [] : playerRelations.rows)].map(relation => ({ + id: relation.id, fromWidgetId: relation.from_widget_id, toWidgetId: relation.to_widget_id, type: relation.relation_type, + sortOrder: relation.sort_order, config: { ...relation.config, ...(relationState.get(relation.id) || {}) }, + })) + const playthrough = playthroughResult.rows[0] + return { id: level.id, title: level.title, subtitle: level.subtitle, documents, evidence, relations, connections, + viewport: playthrough.viewport, updatedAt: playthrough.updated_at.toISOString(), levelStatus: level.status, editingAllowed: editingEnabled } +} + +async function savePlaythrough(client: PoolClient, state: CaseState) { + const playthroughId = `default:${state.id}` + const authored = await client.query<{ id: string }>('SELECT id FROM osint.widgets WHERE level_id = $1', [state.id]) + const authoredIds = new Set(authored.rows.map(row => row.id)) + const authoredRelations = await client.query<{ id: string }>('SELECT id FROM osint.widget_relations WHERE level_id = $1', [state.id]) + const authoredRelationIds = new Set(authoredRelations.rows.map(row => row.id)) + await client.query('UPDATE osint.playthroughs SET viewport = $2::jsonb, updated_at = NOW() WHERE id = $1', [playthroughId, JSON.stringify(state.viewport)]) + await client.query('DELETE FROM osint.playthrough_widget_state WHERE playthrough_id = $1', [playthroughId]) + await client.query('DELETE FROM osint.playthrough_widget_relation_state WHERE playthrough_id = $1', [playthroughId]) + await client.query('DELETE FROM osint.playthrough_widget_relations WHERE playthrough_id = $1', [playthroughId]) + await client.query('DELETE FROM osint.playthrough_widgets WHERE playthrough_id = $1', [playthroughId]) + for (const document of state.documents) { + await client.query(`INSERT INTO osint.playthrough_widget_state (playthrough_id, widget_id, x, y, width, config) + VALUES ($1,$2,0,0,0,$3::jsonb)`, [playthroughId, document.id, JSON.stringify({ title: document.title, publishedAt: document.publishedAt || null, fileType: document.fileType, metadata: document.metadata })]) + } + for (const widget of state.evidence) { + if (authoredIds.has(widget.id)) { + await client.query(`INSERT INTO osint.playthrough_widget_state (playthrough_id, widget_id, x, y, width, config) + VALUES ($1, $2, $3, $4, $5, $6::jsonb)`, [playthroughId, widget.id, widget.x, widget.y, widget.width, JSON.stringify({ ...(widget.config || {}), title: widget.title, content: widget.content })]) + } else { + await client.query(`INSERT INTO osint.playthrough_widgets + (id, playthrough_id, widget_type, title, content, config, source_widget_id, source_region_key, event_date, x, y, width) + VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11,$12)`, [widget.id, playthroughId, widget.type, widget.title, widget.content, JSON.stringify(widget.config || {}), + widget.sourceDocumentId || null, widget.sourceRegionId || null, widget.eventDate || null, widget.x, widget.y, widget.width]) + } + } + const fallbackRelations: WidgetRelation[] = state.evidence.flatMap(widget => (widget.containedDocumentIds || []).map((documentId, index) => ({ id: `contains:${widget.id}:${documentId}`, fromWidgetId: widget.id, toWidgetId: documentId, type: 'contains', sortOrder: index }))) + for (const relation of (state.relations || fallbackRelations).filter(relation => authoredRelationIds.has(relation.id))) { + await client.query(`INSERT INTO osint.playthrough_widget_relation_state (playthrough_id, relation_id, config) + VALUES ($1,$2,$3::jsonb)`, [playthroughId, relation.id, JSON.stringify(relation.config || {})]) + } + for (const relation of (state.relations || fallbackRelations).filter(relation => !authoredRelationIds.has(relation.id))) { + await client.query(`INSERT INTO osint.playthrough_widget_relations + (id, playthrough_id, from_widget_id, to_widget_id, relation_type, sort_order, config) + VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)`, + [relation.id, playthroughId, relation.fromWidgetId, relation.toWidgetId, relation.type, relation.sortOrder || 0, JSON.stringify(relation.config || {})]) + } + await client.query('DELETE FROM osint.playthrough_connections WHERE playthrough_id = $1', [playthroughId]) + const authoredConnections = await client.query<{ id: string }>('SELECT id FROM osint.level_connections WHERE level_id = $1', [state.id]) + const authoredConnectionIds = new Set(authoredConnections.rows.map(row => row.id)) + for (const connection of state.connections.filter(c => !authoredConnectionIds.has(c.id))) { + await client.query(`INSERT INTO osint.playthrough_connections (id, playthrough_id, from_widget_id, to_widget_id) + VALUES ($1, $2, $3, $4)`, [connection.id, playthroughId, connection.fromEvidenceId, connection.toEvidenceId]) + } +} + +async function saveAuthoredLevel(client: PoolClient, state: CaseState) { + await client.query('UPDATE osint.levels SET title = $2, subtitle = $3, updated_at = NOW() WHERE id = $1', [state.id, state.title, state.subtitle]) + await client.query('DELETE FROM osint.level_connections WHERE level_id = $1', [state.id]) + await client.query('DELETE FROM osint.widget_relations WHERE level_id = $1', [state.id]) + await client.query('DELETE FROM osint.widgets WHERE level_id = $1', [state.id]) + for (const [index, doc] of state.documents.entries()) { + await client.query(`INSERT INTO osint.widgets (id, level_id, widget_type, title, config, published_at, asset_id, sort_order) + VALUES ($1,$2,'document',$3,$4::jsonb,$5,$6,$7)`, [doc.id, state.id, doc.title, JSON.stringify({ kind: doc.kind, body: doc.body, fileType: doc.fileType, metadata: doc.metadata }), doc.publishedAt || doc.date || null, doc.assetId || null, index]) + for (const [regionIndex, region] of doc.regions.entries()) { + await client.query(`INSERT INTO osint.widget_regions (id, document_widget_id, region_key, label, excerpt, event_date, sort_order) + VALUES ($1,$2,$3,$4,$5,$6,$7)`, [`${doc.id}:${region.id}`, doc.id, region.id, region.label, region.excerpt, region.date || null, regionIndex]) + } + } + for (const [index, widget] of state.evidence.entries()) { + await client.query(`INSERT INTO osint.widgets + (id, level_id, widget_type, title, content, config, source_widget_id, source_region_key, event_date, x, y, width, sort_order) + VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11,$12,$13)`, [widget.id, state.id, widget.type, widget.title, widget.content, JSON.stringify(widget.config || {}), + widget.sourceDocumentId || null, widget.sourceRegionId || null, widget.eventDate || null, widget.x, widget.y, widget.width, index]) + } + const fallbackRelations: WidgetRelation[] = state.evidence.flatMap(widget => (widget.containedDocumentIds || []).map((documentId, index) => ({ id: `contains:${widget.id}:${documentId}`, fromWidgetId: widget.id, toWidgetId: documentId, type: 'contains', sortOrder: index }))) + for (const relation of state.relations || fallbackRelations) { + await client.query(`INSERT INTO osint.widget_relations + (id, level_id, from_widget_id, to_widget_id, relation_type, sort_order, config) + VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)`, + [relation.id, state.id, relation.fromWidgetId, relation.toWidgetId, relation.type, relation.sortOrder || 0, JSON.stringify(relation.config || {})]) + } + for (const connection of state.connections) { + await client.query(`INSERT INTO osint.level_connections (id, level_id, from_widget_id, to_widget_id) + VALUES ($1,$2,$3,$4)`, [connection.id, state.id, connection.fromEvidenceId, connection.toEvidenceId]) + } +} + +const app = express() +app.disable('x-powered-by') +app.use(cors({ origin: process.env.CORS_ORIGIN || true })) +app.use(express.json({ limit: '2mb' })) +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: Number(process.env.MAX_DOCUMENT_BYTES || 25 * 1024 * 1024), files: 1 }, +}) + +app.get('/api/health', async (_req, res) => { + try { await pool.query('SELECT 1'); res.json({ ok: true, database: 'connected', schema: 'osint', editingEnabled }) } + catch { res.status(503).json({ ok: false, database: 'unavailable' }) } +}) +app.get('/api/levels', async (_req, res, next) => { + try { const result = await pool.query('SELECT id, title, subtitle, status, updated_at AS "updatedAt" FROM osint.levels ORDER BY updated_at DESC'); res.json(result.rows) } + catch (error) { next(error) } +}) +app.post('/api/levels', async (req, res, next) => { + try { + if (!editingEnabled) return res.status(403).json({ error: 'Level editing is disabled' }) + const title = String(req.body?.title || 'Untitled Investigation').trim() + const id = String(req.body?.id || `level-${Date.now()}`).trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-') + await pool.query('INSERT INTO osint.levels (id, title, subtitle) VALUES ($1, $2, $3)', [id, title, String(req.body?.subtitle || '')]) + const level = await assembleLevel(id, `default:${id}`, true) + res.status(201).json(level) + } catch (error) { next(error) } +}) +app.get('/api/assets/:id', async (req, res, next) => { + try { + const result = await pool.query<{ original_name: string; mime_type: string; byte_size: string; content: Buffer }>( + 'SELECT original_name, mime_type, byte_size, content FROM osint.assets WHERE id = $1', [req.params.id], + ) + const asset = result.rows[0] + if (!asset) return res.status(404).json({ error: 'Asset not found' }) + const inline = asset.mime_type === 'application/pdf' || asset.mime_type.startsWith('image/') || asset.mime_type.startsWith('text/') + res.setHeader('Content-Type', asset.mime_type || 'application/octet-stream') + res.setHeader('Content-Length', asset.byte_size) + res.setHeader('Content-Disposition', `${inline ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(asset.original_name)}`) + res.setHeader('X-Content-Type-Options', 'nosniff') + res.send(asset.content) + } catch (error) { next(error) } +}) +app.post('/api/levels/:id/documents', upload.single('file'), async (req, res, next) => { + if (!wantsEdit(req)) return res.status(403).json({ error: 'Level editing is disabled' }) + if (!req.file) return res.status(400).json({ error: 'A file is required' }) + const client = await pool.connect() + try { + const level = await client.query('SELECT id FROM osint.levels WHERE id = $1', [req.params.id]) + if (!level.rows[0]) return res.status(404).json({ error: 'Level not found' }) + const assetId = `asset-${randomUUID()}` + const widgetId = `document-${randomUUID()}` + const checksum = createHash('sha256').update(req.file.buffer).digest('hex') + const kind = req.file.mimetype === 'application/pdf' ? 'PDF' : req.file.mimetype.startsWith('image/') ? 'IMAGE' : 'FILE' + const fileType: CaseDocument['fileType'] = req.file.mimetype.startsWith('image/') ? 'image' : req.file.mimetype === 'application/pdf' ? 'pdf' : 'file' + await client.query('BEGIN') + await client.query(`INSERT INTO osint.assets (id, level_id, original_name, mime_type, byte_size, content, checksum_sha256) + VALUES ($1,$2,$3,$4,$5,$6,$7)`, [assetId, req.params.id, req.file.originalname, req.file.mimetype || 'application/octet-stream', req.file.size, req.file.buffer, checksum]) + await client.query(`INSERT INTO osint.widgets (id, level_id, widget_type, title, config, asset_id, sort_order) + VALUES ($1,$2,'document',$3,$4::jsonb,$5,(SELECT COALESCE(MAX(sort_order),-1)+1 FROM osint.widgets WHERE level_id=$2 AND widget_type='document'))`, + [widgetId, req.params.id, req.file.originalname, JSON.stringify({ kind, body: [], fileType, metadata: {} }), assetId]) + await client.query('UPDATE osint.levels SET updated_at = NOW() WHERE id = $1', [req.params.id]) + await client.query('COMMIT') + res.status(201).json({ id: widgetId, title: req.file.originalname, kind, fileType, metadata: {}, date: '', body: [], regions: [], assetId, + fileName: req.file.originalname, mimeType: req.file.mimetype, fileSize: req.file.size }) + } catch (error) { await client.query('ROLLBACK'); next(error) } finally { client.release() } +}) +app.get('/api/levels/:id', async (req, res, next) => { + try { const level = await assembleLevel(req.params.id, `default:${req.params.id}`, wantsEdit(req)); level ? res.json(level) : res.status(404).json({ error: 'Level not found' }) } + catch (error) { next(error) } +}) +app.put('/api/levels/:id', async (req, res, next) => { + const state = req.body as CaseState + if (!state || state.id !== req.params.id || !Array.isArray(state.evidence) || !Array.isArray(state.connections)) return res.status(400).json({ error: 'Invalid level state' }) + const client = await pool.connect() + try { + await client.query('BEGIN') + if (wantsEdit(req)) await saveAuthoredLevel(client, state) + else await savePlaythrough(client, state) + await client.query('COMMIT'); res.json({ ok: true, mode: wantsEdit(req) ? 'author' : 'play' }) + } catch (error) { await client.query('ROLLBACK'); next(error) } finally { client.release() } +}) +app.post('/api/levels/:id/reset', async (req, res, next) => { + const client = await pool.connect() + try { + const playthroughId = `default:${req.params.id}` + await client.query('BEGIN') + await client.query('DELETE FROM osint.playthroughs WHERE id = $1', [playthroughId]) + await client.query('COMMIT') + const level = await assembleLevel(req.params.id); level ? res.json(level) : res.status(404).json({ error: 'Level not found' }) + } catch (error) { await client.query('ROLLBACK'); next(error) } finally { client.release() } +}) + +app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + if (error instanceof multer.MulterError) { + return res.status(error.code === 'LIMIT_FILE_SIZE' ? 413 : 400).json({ error: error.code === 'LIMIT_FILE_SIZE' ? 'Document exceeds the upload limit' : error.message }) + } + console.error(error); res.status(500).json({ error: 'Internal server error' }) +}) +const here = path.dirname(fileURLToPath(import.meta.url)); const dist = path.resolve(here, '..', 'dist') +if (fs.existsSync(dist)) { app.use(express.static(dist)); app.get('*splat', (_req, res) => res.sendFile(path.join(dist, 'index.html'))) } +const port = Number(process.env.PORT || 8787) +const server = app.listen(port, '0.0.0.0', () => console.log(`GUPI OSINT Board listening on http://localhost:${port}`)) +async function shutdown() { server.close(); await pool.end(); process.exit(0) } +process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown) diff --git a/server/migrate.ts b/server/migrate.ts new file mode 100644 index 0000000..5c38f5a --- /dev/null +++ b/server/migrate.ts @@ -0,0 +1,51 @@ +import 'dotenv/config' +import { createHash } from 'node:crypto' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import pg from 'pg' + +const { Client } = pg +const databaseUrl = process.env.DATABASE_URL +if (!databaseUrl) { + console.error('DATABASE_URL is required') + process.exit(1) +} + +const migrationsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'migrations') +const client = new Client({ connectionString: databaseUrl }) +await client.connect() + +try { + await client.query('CREATE SCHEMA IF NOT EXISTS osint') + await client.query(`CREATE TABLE IF NOT EXISTS osint.schema_migrations ( + name TEXT PRIMARY KEY, + checksum TEXT NOT NULL, + applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + )`) + + const files = (await fs.readdir(migrationsDir)).filter(name => /^\d+.*\.sql$/.test(name)).sort() + for (const name of files) { + const sql = await fs.readFile(path.join(migrationsDir, name), 'utf8') + const checksum = createHash('sha256').update(sql).digest('hex') + const existing = await client.query<{ checksum: string }>('SELECT checksum FROM osint.schema_migrations WHERE name = $1', [name]) + if (existing.rows[0]) { + if (existing.rows[0].checksum !== checksum) throw new Error(`Applied migration was modified: ${name}`) + console.log(`skip ${name}`) + continue + } + await client.query('BEGIN') + try { + await client.query(sql) + await client.query('INSERT INTO osint.schema_migrations (name, checksum) VALUES ($1, $2)', [name, checksum]) + await client.query('COMMIT') + console.log(`apply ${name}`) + } catch (error) { + await client.query('ROLLBACK') + throw error + } + } + console.log('OSINT migrations complete.') +} finally { + await client.end() +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..d7305ce --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,457 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { BookOpen, CalendarClock, ChevronRight, CircleHelp, FileText, Folder, FolderOpen, Hand, Image as ImageIcon, Link2, Minus, MousePointer2, Network, NotebookPen, Pencil, Plus, RotateCcw, Search, Trash2, Upload, X, ZoomIn, ZoomOut } from 'lucide-react' +import type { CaseDocument, CaseState, Evidence, SourceFileType, WidgetRelation } from './types' + +const BOARD_W = 2400 +const BOARD_H = 1500 +const SOURCE_FILE_TYPES: { value: SourceFileType; label: string }[] = [ + { value: 'image', label: 'Image' }, { value: 'pdf', label: 'PDF' }, { value: 'web_capture', label: 'Web capture' }, + { value: 'email', label: 'Email' }, { value: 'article', label: 'Article' }, { value: 'filing', label: 'Company filing' }, + { value: 'price_list', label: 'Price list' }, { value: 'text', label: 'Text document' }, { value: 'file', label: 'Generic file' }, +] + +function uid(prefix: string) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}` } +function connectionPoint(item: Evidence) { + return item.type === 'note' ? { x: item.x + 54, y: item.y + 12 } : { x: item.x + item.width / 2, y: item.y + 68 } +} + +type TemporalItem = { id: string; sourceTemporalId: string; date: string; label: string; kind: 'document' | 'widget'; evidenceId?: string; documentId?: string } + +function normalizeCase(state: CaseState): CaseState { + const relations = Array.isArray(state.relations) ? state.relations : state.evidence.flatMap(widget => (widget.containedDocumentIds || (widget.sourceDocumentId ? [widget.sourceDocumentId] : [])).map((documentId, index) => ({ id: `contains:${widget.id}:${documentId}`, fromWidgetId: widget.id, toWidgetId: documentId, type: 'contains', sortOrder: index }))) + const membership = new Map() + for (const relation of relations.filter(relation => relation.type === 'contains').sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))) membership.set(relation.fromWidgetId, [...(membership.get(relation.fromWidgetId) || []), relation.toWidgetId]) + return { ...state, relations, + documents: state.documents.map(document => ({ ...document, fileType: document.fileType || (document.mimeType?.startsWith('image/') ? 'image' : document.mimeType === 'application/pdf' ? 'pdf' : 'file'), metadata: document.metadata || {} })), + evidence: state.evidence.map(widget => ({ ...widget, type: widget.type === 'evidence' ? 'folder' : widget.type, config: widget.config || {}, containedDocumentIds: membership.get(widget.id) || [] })) } +} + +function containedIds(state: CaseState, widgetId: string) { + return state.relations.filter(relation => relation.type === 'contains' && relation.fromWidgetId === widgetId).sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)).map(relation => relation.toWidgetId) +} + +function folderIsOpen(folder: Evidence) { return folder.config?.open === true } +function relationPosition(state: CaseState, relation: WidgetRelation) { + const folder = state.evidence.find(widget => widget.id === relation.fromWidgetId) + const order = relation.sortOrder || 0 + const configuredX = Number(relation.config?.x), configuredY = Number(relation.config?.y) + return { + x: Number.isFinite(configuredX) ? configuredX : (folder?.x || 100) + (folder?.width || 240) + 90 + (order % 3) * 205, + y: Number.isFinite(configuredY) ? configuredY : (folder?.y || 100) - 30 + Math.floor(order / 3) * 185, + } +} + +export function App() { + const [caseState, setCaseState] = useState(null) + const [noLevels, setNoLevels] = useState(false) + const [openDoc, setOpenDoc] = useState(null) + const [selected, setSelected] = useState(null) + const [linkFrom, setLinkFrom] = useState(null) + const [docsOpen, setDocsOpen] = useState(true) + const [helpOpen, setHelpOpen] = useState(false) + const [status, setStatus] = useState('CONNECTING TO ARCHIVE…') + const [clock, setClock] = useState('') + const [draggingFiles, setDraggingFiles] = useState(false) + const [uploading, setUploading] = useState(0) + const [boardTool, setBoardTool] = useState<'move' | 'hand'>('move') + const [editingFolderId, setEditingFolderId] = useState(null) + const [editingFileId, setEditingFileId] = useState(null) + const saveTimer = useRef(undefined) + const boardRef = useRef(null) + const fileInputRef = useRef(null) + const requestedEditMode = new URLSearchParams(window.location.search).get('edit') === '1' + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + fetch('/api/levels').then(r => { + if (!r.ok) throw new Error('Server unavailable') + return r.json() + }).then(async (levels: { id: string }[]) => { + const levelId = params.get('level') || levels[0]?.id + if (!levelId) { setNoLevels(true); setStatus('NO LEVELS IN ARCHIVE'); return } + const editQuery = params.get('edit') === '1' ? '?edit=1' : '' + const response = await fetch(`/api/levels/${encodeURIComponent(levelId)}${editQuery}`) + if (!response.ok) throw new Error('Level unavailable') + const data = await response.json() + setCaseState(normalizeCase(data)); setStatus('EVIDENCE INTEGRITY: PROBABLY OK') + }) + .catch(() => { + const cached = localStorage.getItem('gupi-osint-board:last') + if (cached) setCaseState(normalizeCase(JSON.parse(cached))) + setStatus(cached ? 'OFFLINE · LOCAL COPY' : 'SERVER UNAVAILABLE') + }) + const tick = () => setClock(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })) + tick(); const timer = window.setInterval(tick, 30000) + return () => clearInterval(timer) + }, []) + + const update = useCallback((fn: (state: CaseState) => CaseState) => { + setCaseState(current => { + if (!current) return current + const next = fn(current) + localStorage.setItem('gupi-osint-board:last', JSON.stringify(next)) + window.clearTimeout(saveTimer.current) + saveTimer.current = window.setTimeout(() => { + const editQuery = requestedEditMode && next.editingAllowed ? '?edit=1' : '' + fetch(`/api/levels/${encodeURIComponent(next.id)}${editQuery}`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(next) }) + .then(r => { if (!r.ok) throw new Error(); setStatus('SAVED TO CASE ARCHIVE') }) + .catch(() => setStatus('OFFLINE · SAVED LOCALLY')) + }, 450) + return next + }) + }, [requestedEditMode]) + + const focusEvidence = (id: string) => { + if (!caseState) return + const ev = caseState.evidence.find(e => e.id === id) + if (!ev) return + setSelected(id) + update(s => ({ ...s, viewport: { ...s.viewport, x: 500 - ev.x * s.viewport.zoom, y: 260 - ev.y * s.viewport.zoom } })) + } + + const extract = (doc: CaseDocument, regionId: string) => { + if (!caseState) return + const region = doc.regions.find(r => r.id === regionId)! + const existing = caseState.evidence.find(e => e.sourceDocumentId === doc.id && e.sourceRegionId === regionId) + if (existing) { setOpenDoc(null); focusEvidence(existing.id); return } + const ev: Evidence = { + id: uid('folder'), type: 'folder', title: `${doc.kind} EVIDENCE`, content: region.excerpt, config: { open: false }, + sourceDocumentId: doc.id, sourceRegionId: region.id, containedDocumentIds: [doc.id], + x: 850 + Math.random() * 220, y: 390 + Math.random() * 250, width: 260, + } + update(s => ({ ...s, evidence: [...s.evidence, ev], relations: [...s.relations, { id: `contains:${ev.id}:${doc.id}`, fromWidgetId: ev.id, toWidgetId: doc.id, type: 'contains', sortOrder: 0 }] })) + setOpenDoc(null); setSelected(ev.id); setStatus('EVIDENCE EXTRACTED · PROVENANCE ATTACHED') + } + + const addNote = () => { + const content = window.prompt('What do you think this evidence means?')?.trim() + if (!content || !caseState) return + const { viewport } = caseState + const note: Evidence = { id: uid('note'), type: 'note', title: 'WORKING NOTE', content, x: Math.max(100, (500 - viewport.x) / viewport.zoom), y: Math.max(100, (330 - viewport.y) / viewport.zoom), width: 108 } + update(s => ({ ...s, evidence: [...s.evidence, note] })); setSelected(note.id) + } + + const handleCardClick = (id: string) => { + if (!linkFrom) { setSelected(current => current === id ? null : id); return } + if (linkFrom !== id && caseState && !caseState.connections.some(c => (c.fromEvidenceId === linkFrom && c.toEvidenceId === id) || (c.fromEvidenceId === id && c.toEvidenceId === linkFrom))) { + update(s => ({ ...s, connections: [...s.connections, { id: uid('connection'), fromEvidenceId: linkFrom, toEvidenceId: id }] })) + } + setLinkFrom(null); setSelected(id) + } + + const reset = async () => { + if (!window.confirm('Reset the entire investigation board?')) return + const response = await fetch(`/api/levels/${encodeURIComponent(caseState!.id)}/reset`, { method: 'POST' }) + if (response.ok) { const data = await response.json(); setCaseState(normalizeCase(data)); localStorage.removeItem('gupi-osint-board:last'); setSelected(null); setStatus('CASE RESET') } + } + + const uploadFiles = async (files: FileList | File[]) => { + if (!caseState || !requestedEditMode || !caseState.editingAllowed) return + const queue = Array.from(files) + setUploading(queue.length) + setDraggingFiles(false) + for (const file of queue) { + const form = new FormData() + form.append('file', file) + try { + const response = await fetch(`/api/levels/${encodeURIComponent(caseState.id)}/documents?edit=1`, { method: 'POST', body: form }) + if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error || `Upload failed (${response.status})`) } + const document: CaseDocument = await response.json() + update(s => ({ ...s, documents: [...s.documents, document] })) + setStatus(`IMPORTED · ${file.name.toUpperCase()}`) + } catch (error) { + setStatus(error instanceof Error ? error.message.toUpperCase() : 'UPLOAD FAILED') + } finally { setUploading(count => count - 1) } + } + } + + if (noLevels) return { setCaseState(level); setNoLevels(false); window.history.replaceState({}, '', `?level=${encodeURIComponent(level.id)}&edit=1`) }} /> + if (!caseState) return
GU

GLITCH UNIVERSITY NETWORK TERMINAL

{status}
+ + const documentById = new Map(caseState.documents.map(document => [document.id, document])) + const containedDocumentIds = new Set(caseState.relations.filter(relation => relation.type === 'contains').map(relation => relation.toWidgetId)) + const temporalItems: TemporalItem[] = [ + ...caseState.evidence.flatMap(folder => folder.type !== 'folder' ? [] : containedIds(caseState, folder.id).flatMap(documentId => { + const document = documentById.get(documentId) + const date = document?.publishedAt || document?.date + const relation = caseState.relations.find(candidate => candidate.type === 'contains' && candidate.fromWidgetId === folder.id && candidate.toWidgetId === documentId) + return document && date && relation ? [{ id: `folder:${folder.id}:document:${document.id}`, sourceTemporalId: folderIsOpen(folder) ? `file:${relation.id}` : `widget:${folder.id}`, date, label: document.title, kind: 'document' as const, evidenceId: folder.id, documentId: document.id }] : [] + })), + ...caseState.documents.filter(document => !containedDocumentIds.has(document.id) && (document.publishedAt || document.date)).map(document => ({ id: `document:${document.id}`, sourceTemporalId: `document:${document.id}`, date: document.publishedAt || document.date, label: document.title, kind: 'document' as const, documentId: document.id })), + ...caseState.evidence.filter(widget => widget.type === 'event' && widget.eventDate).map(widget => ({ id: `widget:${widget.id}`, sourceTemporalId: `widget:${widget.id}`, date: widget.eventDate!, label: widget.content, kind: 'widget' as const, evidenceId: widget.id })), + ].sort((a, b) => dateValue(a.date) - dateValue(b.date)) + return
+
+
GUOSINT BOARD / {requestedEditMode && caseState.editingAllowed ? 'LEVEL EDITOR' : 'CASE TERMINAL'}
+ +
{status}{clock}
+
+ +
+ + +
{ if (e.dataTransfer.types.includes('Files') && requestedEditMode && caseState.editingAllowed) { e.preventDefault(); setDraggingFiles(true) } }} onDragOver={e => { if (requestedEditMode && caseState.editingAllowed) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy' } }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDraggingFiles(false) }} onDrop={e => { e.preventDefault(); if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files) }}> +
{requestedEditMode && caseState.editingAllowed ? 'AUTHORING LEVEL' : 'ACTIVE INVESTIGATION'}

{caseState.title}

{caseState.subtitle || caseState.id.toUpperCase()}

{requestedEditMode && caseState.editingAllowed ? 'DRAFT' : 'CASE'}
{caseState.levelStatus?.toUpperCase() || 'ACTIVE'}
+ setOpenDoc(caseState.documents.find(d => d.id === id) || null)} onEditFolder={setEditingFolderId} onEditFile={setEditingFileId} /> + {!docsOpen && } +
+ + + + + + + + {Math.round(caseState.viewport.zoom * 100)}% + + +
+ {draggingFiles &&
ADD SOURCE DOCUMENTSDROP FILES INTO THIS LEVEL
} +
+
+ + `${e.id}:${e.x}:${e.y}:${String(e.config?.open)}`).join('|')}:${caseState.relations.map(r => `${r.id}:${String(r.config?.x)}:${String(r.config?.y)}`).join('|')}`}/> + { if (item.documentId) setOpenDoc(caseState.documents.find(doc => doc.id === item.documentId) || null); else if (item.evidenceId) focusEvidence(item.evidenceId) }}/> + {openDoc && setOpenDoc(null)} onExtract={id => extract(openDoc, id)} extracted={caseState.evidence.filter(e => e.sourceDocumentId === openDoc.id).map(e => e.sourceRegionId)} />} + {editingFolderId && widget.id === editingFolderId)!} memberIds={containedIds(caseState, editingFolderId)} documents={caseState.documents} canManageContents={requestedEditMode && Boolean(caseState.editingAllowed)} onClose={() => setEditingFolderId(null)} onSave={(folder, members) => { update(state => ({ ...state, evidence: state.evidence.map(widget => widget.id === folder.id ? { ...folder, containedDocumentIds: members } : widget), relations: [...state.relations.filter(relation => relation.type !== 'contains' || relation.fromWidgetId !== folder.id), ...members.map((documentId, index) => { const existing = state.relations.find(relation => relation.type === 'contains' && relation.fromWidgetId === folder.id && relation.toWidgetId === documentId); const position = relationPosition(state, existing || { id: '', fromWidgetId: folder.id, toWidgetId: documentId, type: 'contains', sortOrder: index }); return { id: existing?.id || `contains:${folder.id}:${documentId}`, fromWidgetId: folder.id, toWidgetId: documentId, type: 'contains', sortOrder: index, config: existing?.config || position } })] })); setEditingFolderId(null); setStatus('FOLDER UPDATED') }}/>} + {editingFileId && document.id === editingFileId)!} onClose={() => setEditingFileId(null)} onSave={document => { update(state => ({ ...state, documents: state.documents.map(candidate => candidate.id === document.id ? document : candidate) })); setEditingFileId(null); setStatus('FILE METADATA UPDATED') }}/>} + {helpOpen && setHelpOpen(false)}/>} +
+} + +function EmptyArchive({ canEdit, onCreated }: { canEdit: boolean; onCreated: (level: CaseState) => void }) { + const [creating, setCreating] = useState(false) + const createLevel = async () => { + const title = window.prompt('Name this investigation level:', 'Untitled Investigation')?.trim() + if (!title) return + setCreating(true) + try { + const response = await fetch('/api/levels', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title }) }) + if (!response.ok) throw new Error('Could not create level') + onCreated(await response.json()) + } finally { setCreating(false) } + } + return
GU
GLITCH UNIVERSITY LEVEL ARCHIVE

No investigations found.

The database is ready, but no authored level exists yet.

{canEdit ? :

Add ?edit=1 and enable level editing on the server to begin authoring.

}
+} + +function Board({ state, selected, linkFrom, tool, boardRef, update, onCardClick, onOpenSource, onEditFolder, onEditFile }: { state: CaseState; selected: string | null; linkFrom: string | null; tool: 'move' | 'hand'; boardRef: React.RefObject; update: (fn: (s: CaseState) => CaseState) => void; onCardClick: (id: string) => void; onOpenSource: (id: string) => void; onEditFolder: (id: string) => void; onEditFile: (id: string) => void }) { + const drag = useRef<{ kind: 'pan' | 'widget' | 'relation'; id?: string; startX: number; startY: number; originX: number; originY: number; moved?: boolean } | null>(null) + const suppressClick = useRef(false) + const byId = useMemo(() => new Map(state.evidence.map(e => [e.id, e])), [state.evidence]) + const containmentRelations = state.relations.filter(relation => relation.type === 'contains') + useEffect(() => { + const board = boardRef.current + if (!board) return + const handlePinch = (event: WheelEvent) => { + if (!event.ctrlKey && !event.metaKey) return + event.preventDefault() + event.stopPropagation() + update(s => ({ ...s, viewport: { ...s.viewport, zoom: Math.max(.45, Math.min(1.5, s.viewport.zoom - event.deltaY * .006)) } })) + } + board.addEventListener('wheel', handlePinch, { passive: false }) + return () => board.removeEventListener('wheel', handlePinch) + }, [boardRef, update]) + const pointerDown = (event: React.PointerEvent, target?: { kind: 'widget' | 'relation'; id: string }) => { + if ((event.target as HTMLElement).closest('button')) return + const widget = target?.kind === 'widget' ? byId.get(target.id) : undefined + const relation = target?.kind === 'relation' ? state.relations.find(candidate => candidate.id === target.id) : undefined + const position = relation ? relationPosition(state, relation) : undefined + drag.current = { kind: target?.kind || 'pan', id: target?.id, startX: event.clientX, startY: event.clientY, originX: widget?.x ?? position?.x ?? state.viewport.x, originY: widget?.y ?? position?.y ?? state.viewport.y } + event.currentTarget.setPointerCapture(event.pointerId) + } + const pointerMove = (event: React.PointerEvent) => { + if (!drag.current) return + const dx = event.clientX - drag.current.startX, dy = event.clientY - drag.current.startY + if (Math.abs(dx) + Math.abs(dy) > 4) drag.current.moved = true + if (drag.current.kind === 'widget') update(s => ({ ...s, evidence: s.evidence.map(e => e.id === drag.current!.id ? { ...e, x: drag.current!.originX + dx / s.viewport.zoom, y: drag.current!.originY + dy / s.viewport.zoom } : e) })) + else if (drag.current.kind === 'relation') update(s => ({ ...s, relations: s.relations.map(relation => relation.id === drag.current!.id ? { ...relation, config: { ...(relation.config || {}), x: drag.current!.originX + dx / s.viewport.zoom, y: drag.current!.originY + dy / s.viewport.zoom } } : relation) })) + else update(s => ({ ...s, viewport: { ...s.viewport, x: drag.current!.originX + dx, y: drag.current!.originY + dy } })) + } + const finishDrag = () => { if (drag.current) suppressClick.current = Boolean(drag.current.moved); drag.current = null } + const toggleFolder = (id: string) => update(s => ({ ...s, evidence: s.evidence.map(widget => widget.id === id ? { ...widget, config: { ...(widget.config || {}), open: !folderIsOpen(widget) } } : widget) })) + return
{ if (tool === 'hand' || e.button === 1) { e.preventDefault(); pointerDown(e) } }} + onPointerMove={pointerMove} onPointerUp={finishDrag} onAuxClick={e => { if (e.button === 1) e.preventDefault() }}> +
+
AUTHORIZED CITIZEN SCIENTIST WORKSTATION GU-NET / 04
+ + {state.connections.map(c => { const a = byId.get(c.fromEvidenceId), b = byId.get(c.toEvidenceId); if (!a || !b) return null; const p1 = connectionPoint(a), p2 = connectionPoint(b); return })} + + + {containmentRelations.map(relation => { const folder = byId.get(relation.fromWidgetId), document = state.documents.find(candidate => candidate.id === relation.toWidgetId); if (!folder || !document) return null; const open = folderIsOpen(folder), position = relationPosition(state, relation), origin = { x: folder.x + folder.width / 2, y: folder.y + 78 }; return })} + + {state.evidence.map((ev, i) => { const containedDocuments = containedIds(state, ev.id).flatMap(id => { const document = state.documents.find(candidate => candidate.id === id); return document ? [document] : [] }); return
{ e.stopPropagation(); if (tool === 'hand' || e.button === 1) { e.preventDefault(); pointerDown(e) } else if (e.button === 0) pointerDown(e, { kind: 'widget', id: ev.id }) }} + onPointerMove={e => { e.stopPropagation(); pointerMove(e) }} onPointerUp={e => { e.stopPropagation(); finishDrag() }} + onAuxClick={e => { if (e.button === 1) e.preventDefault() }} onClick={e => { e.stopPropagation(); if (suppressClick.current) { suppressClick.current = false; return } if (tool === 'move') onCardClick(ev.id) }}> +
{ev.type === 'note' ? 'INVESTIGATOR / NOTE' : ev.type === 'folder' ? `EVIDENCE FOLDER / ${containedDocuments.length}` : ev.type.toUpperCase()}{String(i + 1).padStart(3, '0')}
+

{ev.title}

{ev.content}

+ {ev.eventDate && } + {ev.type === 'folder' && !folderIsOpen(ev) &&
{containedDocuments.slice(0, 3).map(document => )}{containedDocuments.length > 3 && + {containedDocuments.length - 3} MORE FILES}
} + {ev.type === 'folder' &&
} + {ev.type !== 'folder' && ev.sourceDocumentId && }
+
})} + {containmentRelations.map(relation => { const folder = byId.get(relation.fromWidgetId), document = state.documents.find(candidate => candidate.id === relation.toWidgetId); if (!folder || !document) return null; const open = folderIsOpen(folder), target = relationPosition(state, relation); const left = open ? target.x : folder.x + folder.width / 2 - 87, top = open ? target.y : folder.y + 45; return
{ event.stopPropagation(); if (tool === 'hand' || event.button === 1) { event.preventDefault(); pointerDown(event) } else if (open && event.button === 0) pointerDown(event, { kind: 'relation', id: relation.id }) }} + onPointerMove={event => { event.stopPropagation(); pointerMove(event) }} onPointerUp={event => { event.stopPropagation(); finishDrag() }} onDoubleClick={() => open && onOpenSource(document.id)}> +
{document.fileType.replaceAll('_', ' ').toUpperCase()}{String((relation.sortOrder || 0) + 1).padStart(2, '0')}
+
{document.fileType === 'image' && document.assetId ? :
{document.kind}
}
+ {document.title} +
+
})} +
+
+} + +function TemporalLinks({ items, layoutKey }: { items: TemporalItem[]; layoutKey: string }) { + const [lines, setLines] = useState<{ id: string; x1: number; y1: number; x2: number; y2: number }[]>([]) + const itemsKey = items.map(item => `${item.id}:${item.date}`).join('|') + useLayoutEffect(() => { + let frame = 0 + let animateUntil = Date.now() + 500 + const measure = () => { + cancelAnimationFrame(frame) + frame = requestAnimationFrame(function measureFrame() { + const sources = new Map(Array.from(document.querySelectorAll('[data-temporal-id]')).map(element => [element.dataset.temporalId, element])) + const markers = new Map(Array.from(document.querySelectorAll('[data-marker-id]')).map(element => [element.dataset.markerId, element])) + setLines(items.flatMap(item => { + const source = sources.get(item.sourceTemporalId), marker = markers.get(item.id) + if (!source || !marker) return [] + const from = source.getBoundingClientRect(), to = marker.getBoundingClientRect() + const clip = source.closest('.board-viewport, .doc-list')?.getBoundingClientRect() + const visibleLeft = Math.max(from.left, clip?.left ?? 0), visibleRight = Math.min(from.right, clip?.right ?? window.innerWidth) + const visibleTop = Math.max(from.top, clip?.top ?? 0), visibleBottom = Math.min(from.bottom, clip?.bottom ?? window.innerHeight) + if (visibleRight <= visibleLeft || visibleBottom <= visibleTop) return [] + return [{ id: item.id, x1: visibleLeft + (visibleRight - visibleLeft) / 2, y1: visibleBottom, x2: to.left + to.width / 2, y2: to.top + to.height / 2 }] + })) + if (Date.now() < animateUntil) frame = requestAnimationFrame(measureFrame) + }) + } + measure() + const observer = new ResizeObserver(measure) + document.querySelectorAll('.workspace, .timeline, [data-temporal-id], [data-marker-id]').forEach(element => observer.observe(element)) + const handleResize = () => { animateUntil = Date.now() + 500; measure() } + window.addEventListener('resize', handleResize) + return () => { cancelAnimationFrame(frame); observer.disconnect(); window.removeEventListener('resize', handleResize) } + }, [itemsKey, layoutKey]) + return +} + +function dateValue(date: string) { + const parsed = Date.parse(date) + return Number.isFinite(parsed) ? parsed : 0 +} + +function Timeline({ items, selected, onSelect }: { items: TemporalItem[]; selected: string | null; onSelect: (item: TemporalItem) => void }) { + const itemYears = items.map(item => Number(item.date.slice(0, 4))).filter(Number.isFinite) + let startYear = itemYears.length ? Math.min(...itemYears) : new Date().getFullYear() - 2 + let endYear = itemYears.length ? Math.max(...itemYears) : startYear + 4 + if (endYear - startYear < 4) { const missing = 4 - (endYear - startYear); startYear -= Math.floor(missing / 2); endYear += Math.ceil(missing / 2) } + const start = Date.UTC(startYear, 0, 1), end = Date.UTC(endYear, 11, 31) + const years = Array.from({ length: endYear - startYear + 1 }, (_, index) => startYear + index) + const position = (date: string) => Math.max(0, Math.min(100, ((dateValue(date) - start) / (end - start)) * 100)) + return
TEMPORAL INDEXTIMELINE{items.length} DATED ITEMS
{years.map(year => {year})}{items.map((item, i) => )}
SOURCE SELECTED
+} + +function localDateTime(value?: string) { + if (!value) return '' + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return `${value}T00:00` + const date = new Date(value) + if (!Number.isFinite(date.getTime())) return '' + const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000) + return local.toISOString().slice(0, 16) +} + +function FolderEditor({ folder, memberIds, documents, canManageContents, onClose, onSave }: { folder: Evidence; memberIds: string[]; documents: CaseDocument[]; canManageContents: boolean; onClose: () => void; onSave: (folder: Evidence, members: string[]) => void }) { + const [title, setTitle] = useState(folder.title) + const [content, setContent] = useState(folder.content) + const [members, setMembers] = useState(memberIds) + const toggleMember = (documentId: string) => setMembers(current => current.includes(documentId) ? current.filter(id => id !== documentId) : [...current, documentId]) + const submit = (event: React.FormEvent) => { + event.preventDefault() + onSave({ ...folder, title: title.trim() || 'UNTITLED EVIDENCE FOLDER', content: content.trim(), containedDocumentIds: members }, members) + } + return
+
Edit evidence folder
+
+ FOLDER WIDGET + +