diff --git a/.gitignore b/.gitignore index 58fd031..2313aed 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,12 @@ frontend/.next/ frontend/out/ frontend/node_modules/ +# Backend stack build output (backend/, indexer/, oracle/) +dist/ +backend/dist/ +indexer/dist/ +oracle/dist/ + # ── Environment & Secrets ────────────────────────────────────────────────────── .env .env.local diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e22162 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing to iPredict — Backend & Oracle (`implementation-drips`) + +We're building the real backend and oracle for iPredict. All of this work +happens on the **`implementation-drips`** branch — not `main`. + +## Workflow + +1. **Find an issue.** Browse [open issues](../../issues). Filter by `area:` label + to find work in your wheelhouse: + - `area:backend` / `area:api` — REST API + - `area:indexer` — Soroban event indexer + - `area:db` — database schema, migrations, queries + - `area:oracle` — council aggregator, optimistic oracle, data adapters + - `area:contracts` — Soroban contract changes (oracle state machine, bonds) + - `area:cache` — Redis caching layer + - `area:infra` — Docker, compose, deployment + - `area:monitoring` — metrics, dashboards, alerts + - `area:testing` — tests + - `area:docs` — documentation + - Priority: `P0` (needed for launch), `P1`, `P2`. + +2. **Claim it.** Comment on the issue so others don't duplicate work. + +3. **Branch.** Always branch off `implementation-drips`: + ```bash + git checkout implementation-drips + git pull + git checkout -b feat/ # or fix/, chore/, test/ + ``` + +4. **Build it.** Each issue has Context, What to build, Acceptance criteria, and + the target files/folder. Stay within the issue's scope — small, reviewable PRs. + +5. **Open a PR — base branch `implementation-drips`.** + - Link the issue (`Closes #123`). + - Make sure `npm run typecheck` and `npm test` pass in the package you touched. + +> There is **no CI/GitHub Actions** on this branch yet — checks are manual. +> Please run typecheck/tests locally before requesting review. + +## Repo layout + +``` +backend/ REST API (Node + TypeScript, Fastify) +indexer/ Soroban event indexer (Node + TypeScript) +oracle/ Council aggregator, data adapters, submitter, monitor +db/ Shared Postgres migrations + schema +infra/ Docker compose (dev + production) +contracts/ Soroban smart contracts (existing + new oracle code) +frontend/ Next.js app (existing) +docs/ Architecture & design docs +``` + +## Local setup + +```bash +# 1. Start Postgres + Redis +cd infra && docker compose -f docker-compose.dev.yml up -d + +# 2. Run a service (example: backend) +cd ../backend +cp .env.example .env +npm install +npm run dev +``` + +The design reference for everything is +[`docs/ORACLE_AND_BACKEND.md`](docs/ORACLE_AND_BACKEND.md). diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..64595d4 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,29 @@ +# ── Backend API configuration ──────────────────────────────────────────────── +# Copy to .env and fill in real values. + +# HTTP +PORT=4000 +HOST=0.0.0.0 + +# PostgreSQL (shared with the indexer) +DATABASE_URL=postgres://ipredict:ipredict@localhost:5432/ipredict + +# Redis cache +REDIS_URL=redis://localhost:6379 + +# Stellar / Soroban +SOROBAN_RPC_URL=https://mainnet.sorobanrpc.com +NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015 + +# Contract IDs (from deploy-mainnet-output.json) +MARKET_CONTRACT_ID= +TOKEN_CONTRACT_ID= +REFERRAL_CONTRACT_ID= +LEADERBOARD_CONTRACT_ID= + +# Oracle provider auth (Phase 2) — comma-separated API keys +ORACLE_API_KEYS= + +# Rate limiting defaults (requests per window-seconds) +RATE_LIMIT_DEFAULT=30 +RATE_LIMIT_WINDOW_SECONDS=60 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0bfc137 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,67 @@ +# iPredict Backend API + +REST API that serves market, bet, leaderboard, and stats data to the frontend — +reading from an indexed PostgreSQL copy of on-chain state (written by the +[`indexer/`](../indexer)) with a Redis cache in front, instead of hitting Soroban +RPC directly on every request. + +> **Branch:** all work happens on `implementation-drips`. Open PRs against that +> branch, **not** `main`. + +## Why this exists + +The frontend currently reads markets directly from Soroban RPC. That works for +<100 markets but throttles and slows badly past ~500. This service is the +scalable read path. See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md) +for the full design. + +## Stack + +- **Runtime:** Node.js 20+, TypeScript +- **HTTP:** Fastify +- **DB:** PostgreSQL 16 (shared with the indexer) +- **Cache:** Redis 7 +- **Validation:** Zod +- **Stellar:** `@stellar/stellar-sdk` + +## Layout + +``` +backend/ + src/ + api/ route handlers (markets, leaderboard, stats, oracle) + db/ query layer (shared schema lives in ../db migrations) + cache/ Redis client + cache helpers + config/ env loading & validation + lib/ shared utilities + server.ts Fastify bootstrap + index.ts entrypoint + test/ + package.json + tsconfig.json + .env.example +``` + +## Getting started + +```bash +cd backend +cp .env.example .env # fill in DATABASE_URL, REDIS_URL, contract IDs +npm install +npm run dev # starts the API on :4000 +``` + +You need Postgres and Redis running locally (see +[`infra/`](../infra) for a docker-compose that starts both). + +## Endpoints (target) + +See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#api-endpoints). +Each endpoint is tracked as its own issue. + +## Contributing + +1. Pick an open issue labelled `area:backend` (or `area:api`). +2. Comment to claim it. +3. Branch off `implementation-drips`, implement, open a PR back to + `implementation-drips`. diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..5e3acc5 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1454 @@ +{ + "name": "ipredict-backend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ipredict-backend", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.13.5", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/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/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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/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" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..0e9b7a4 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,33 @@ +{ + "name": "ipredict-backend", + "version": "0.1.0", + "private": true, + "description": "iPredict backend API — indexed market/bet/leaderboard data over REST", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@stellar/stellar-sdk": "^14.5.0", + "fastify": "^4.28.0", + "ioredis": "^5.4.1", + "pg": "^8.12.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "@types/pg": "^8.11.6", + "tsx": "^4.19.0", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } +} diff --git a/backend/src/__tests__/bets.test.ts b/backend/src/__tests__/bets.test.ts new file mode 100644 index 0000000..3f5339e --- /dev/null +++ b/backend/src/__tests__/bets.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { getBetsByMarket, seedBets, clearBets } from "@/db/bets"; +import type { Bet } from "@/db/bets"; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function makeBets(count: number, offset = 0): Bet[] { + return Array.from({ length: count }, (_, i) => ({ + address: `GADDR${offset + i + 1}`, + amount: (offset + i + 1) * 100, + isYes: (offset + i) % 2 === 0, + claimed: false, + })); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe("getBetsByMarket", () => { + beforeEach(() => { + clearBets(); + }); + + it("returns first page of bets", () => { + seedBets(1, makeBets(5)); + + const result = getBetsByMarket(1, 0, 2); + + expect(result.bets).toHaveLength(2); + expect(result.bets[0].address).toBe("GADDR1"); + expect(result.bets[1].address).toBe("GADDR2"); + expect(result.total).toBe(5); + expect(result.page).toBe(0); + expect(result.limit).toBe(2); + expect(result.totalPages).toBe(3); + }); + + it("returns second page", () => { + seedBets(1, makeBets(5)); + + const result = getBetsByMarket(1, 1, 2); + + expect(result.bets).toHaveLength(2); + expect(result.bets[0].address).toBe("GADDR3"); + expect(result.bets[1].address).toBe("GADDR4"); + }); + + it("returns partial last page", () => { + seedBets(1, makeBets(5)); + + const result = getBetsByMarket(1, 2, 2); + + expect(result.bets).toHaveLength(1); + expect(result.bets[0].address).toBe("GADDR5"); + }); + + it("returns empty result for unseeded market", () => { + const result = getBetsByMarket(999, 0, 10); + + expect(result.bets).toEqual([]); + expect(result.total).toBe(0); + expect(result.totalPages).toBe(0); + }); + + it("returns all bets when limit exceeds total", () => { + seedBets(1, makeBets(3)); + + const result = getBetsByMarket(1, 0, 100); + + expect(result.bets).toHaveLength(3); + expect(result.totalPages).toBe(1); + }); + + it("returns empty bets for out-of-range page", () => { + seedBets(1, makeBets(3)); + + const result = getBetsByMarket(1, 10, 2); + + expect(result.bets).toEqual([]); + expect(result.total).toBe(3); + expect(result.totalPages).toBe(2); + }); + + it("returns empty result for negative marketId", () => { + const result = getBetsByMarket(-1, 0, 10); + + expect(result.bets).toEqual([]); + expect(result.total).toBe(0); + }); + + it("returns empty result for negative page", () => { + seedBets(1, makeBets(3)); + + const result = getBetsByMarket(1, -1, 10); + + expect(result.bets).toEqual([]); + }); + + it("returns empty result for zero limit", () => { + seedBets(1, makeBets(3)); + + const result = getBetsByMarket(1, 0, 0); + + expect(result.bets).toEqual([]); + }); + + it("isolates markets from each other", () => { + seedBets(1, makeBets(2)); + seedBets(2, makeBets(3, 2)); + + const r1 = getBetsByMarket(1, 0, 10); + const r2 = getBetsByMarket(2, 0, 10); + + expect(r1.total).toBe(2); + expect(r2.total).toBe(3); + expect(r1.bets[0].address).toBe("GADDR1"); + expect(r2.bets[0].address).toBe("GADDR3"); + }); +}); diff --git a/backend/src/__tests__/leaderboard.test.ts b/backend/src/__tests__/leaderboard.test.ts new file mode 100644 index 0000000..16c7c62 --- /dev/null +++ b/backend/src/__tests__/leaderboard.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + upsertLeaderboardEntry, + getLeaderboardEntry, + clearLeaderboard, +} from "@/db/leaderboard"; + +describe("upsertLeaderboardEntry", () => { + beforeEach(() => { + clearLeaderboard(); + }); + + it("creates a new entry on first upsert", () => { + const result = upsertLeaderboardEntry("GALICE", 100, "won"); + + expect(result.success).toBe(true); + expect(result.hash).toBeDefined(); + + const entry = getLeaderboardEntry("GALICE"); + expect(entry).toEqual({ + address: "GALICE", + points: 100, + won: 1, + lost: 0, + }); + }); + + it("accumulates points and wins across multiple upserts", () => { + upsertLeaderboardEntry("GBOB", 50, "won"); + upsertLeaderboardEntry("GBOB", 30, "won"); + + const entry = getLeaderboardEntry("GBOB"); + expect(entry).toEqual({ + address: "GBOB", + points: 80, + won: 2, + lost: 0, + }); + }); + + it("increments lost counter on loss outcome", () => { + upsertLeaderboardEntry("GCAROL", 20, "lost"); + + const entry = getLeaderboardEntry("GCAROL"); + expect(entry).toEqual({ + address: "GCAROL", + points: 20, + won: 0, + lost: 1, + }); + }); + + it("handles mixed won and lost outcomes", () => { + upsertLeaderboardEntry("GDAVE", 10, "won"); + upsertLeaderboardEntry("GDAVE", 5, "lost"); + upsertLeaderboardEntry("GDAVE", 15, "won"); + + const entry = getLeaderboardEntry("GDAVE"); + expect(entry).toEqual({ + address: "GDAVE", + points: 30, + won: 2, + lost: 1, + }); + }); + + it("returns error for empty address", () => { + const result = upsertLeaderboardEntry("", 10, "won"); + expect(result.success).toBe(false); + expect(result.error).toBe("address is required"); + }); + + it("returns error for whitespace-only address", () => { + const result = upsertLeaderboardEntry(" ", 10, "won"); + expect(result.success).toBe(false); + expect(result.error).toBe("address is required"); + }); + + it("returns error for negative pointsDelta", () => { + const result = upsertLeaderboardEntry("GALICE", -5, "won"); + expect(result.success).toBe(false); + expect(result.error).toBe("pointsDelta must be a non-negative number"); + }); + + it("accepts zero pointsDelta", () => { + const result = upsertLeaderboardEntry("GALICE", 0, "lost"); + expect(result.success).toBe(true); + + const entry = getLeaderboardEntry("GALICE"); + expect(entry!.points).toBe(0); + expect(entry!.lost).toBe(1); + }); +}); diff --git a/backend/src/db/bets.ts b/backend/src/db/bets.ts new file mode 100644 index 0000000..a90f978 --- /dev/null +++ b/backend/src/db/bets.ts @@ -0,0 +1,70 @@ +// ── Types ───────────────────────────────────────────────────────────────────── + +/** A single bet placed by a user on a market. */ +export interface Bet { + address: string; + amount: number; + isYes: boolean; + claimed: boolean; +} + +/** Paginated response from getBetsByMarket. */ +export interface PaginatedBets { + bets: Bet[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +// ── Module-private store ─────────────────────────────────────────────────────── + +const store = new Map(); + +// ── Public API ──────────────────────────────────────────────────────────────── + +/** + * Retrieve a paginated list of bets for a given market. + * + * @param marketId - The market to query (must be >= 0). + * @param page - 0-based page index (must be >= 0). + * @param limit - Number of bets per page (must be > 0). + * @returns Paginated result with bets, total count, and page metadata. + */ +export function getBetsByMarket( + marketId: number, + page: number, + limit: number +): PaginatedBets { + const empty: PaginatedBets = { + bets: [], + total: 0, + page, + limit, + totalPages: 0, + }; + + if (typeof marketId !== "number" || marketId < 0) return empty; + if (typeof page !== "number" || page < 0) return empty; + if (typeof limit !== "number" || limit <= 0) return empty; + + const bets = store.get(marketId) ?? []; + const total = bets.length; + const totalPages = Math.ceil(total / limit); + const offset = page * limit; + const sliced = bets.slice(offset, offset + limit); + + return { bets: sliced, total, page, limit, totalPages }; +} + +// ── Test helpers ────────────────────────────────────────────────────────────── + +/** Seed bets for a market — for test use only. */ +export function seedBets(marketId: number, bets: Bet[]): void { + store.set(marketId, bets); +} + +/** Clear all stored bets — for test isolation only. */ +export function clearBets(): void { + store.clear(); +} diff --git a/backend/src/db/leaderboard.ts b/backend/src/db/leaderboard.ts new file mode 100644 index 0000000..a7384b1 --- /dev/null +++ b/backend/src/db/leaderboard.ts @@ -0,0 +1,83 @@ +// ── Types ───────────────────────────────────────────────────────────────────── + +/** Persistent leaderboard record for a single player. */ +export interface LeaderboardEntry { + address: string; + points: number; + won: number; + lost: number; +} + +/** Outcome of an upsert operation. */ +export interface TransactionResult { + success: boolean; + hash?: string; + error?: string; +} + +// ── Module-private store ─────────────────────────────────────────────────────── + +const store = new Map(); + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function generateId(): string { + return `lb_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; +} + +// ── Public API ──────────────────────────────────────────────────────────────── + +/** + * Insert or update a leaderboard entry. + * + * - If the player does not exist, creates a new entry. + * - Adds `pointsDelta` to the current points. + * - Increments `won` when `outcome` is `"won"`, otherwise increments `lost`. + * + * @param address - Stellar account address (must be non-empty). + * @param pointsDelta - Points to add (must be >= 0). + * @param outcome - Whether the player won or lost. + * @returns `TransactionResult` indicating success or failure. + */ +export function upsertLeaderboardEntry( + address: string, + pointsDelta: number, + outcome: "won" | "lost" +): TransactionResult { + if (!address || address.trim().length === 0) { + return { success: false, error: "address is required" }; + } + + if (typeof pointsDelta !== "number" || pointsDelta < 0) { + return { success: false, error: "pointsDelta must be a non-negative number" }; + } + + const existing = store.get(address); + const entry: LeaderboardEntry = existing + ? { ...existing } + : { address, points: 0, won: 0, lost: 0 }; + + entry.points += pointsDelta; + if (outcome === "won") { + entry.won += 1; + } else { + entry.lost += 1; + } + + store.set(address, entry); + + return { success: true, hash: generateId() }; +} + +/** Retrieve the current leaderboard entry for a player (or undefined). */ +export function getLeaderboardEntry( + address: string +): LeaderboardEntry | undefined { + const entry = store.get(address); + return entry ? { ...entry } : undefined; +} + +/** Clear all entries — for test isolation only. */ +export function clearLeaderboard(): void { + store.clear(); +} diff --git a/backend/src/db/markets.test.ts b/backend/src/db/markets.test.ts new file mode 100644 index 0000000..f2f8714 --- /dev/null +++ b/backend/src/db/markets.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getMarkets, type MarketRow, type Queryable } from "./markets"; + +describe("getMarkets", () => { + it("returns paginated markets rows and total count", async () => { + const marketRows: MarketRow[] = [ + { + id: 42, + question: "Will XLM close above $1 by year end?", + image_url: null, + category: "Crypto", + end_time: "1735689600", + total_yes: "10.0000000", + total_no: "5.0000000", + resolved: false, + outcome: null, + cancelled: false, + creator: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + bet_count: 3, + created_at: new Date("2026-01-01T00:00:00.000Z"), + updated_at: new Date("2026-01-01T00:00:00.000Z") + } + ]; + + const queryMock = vi + .fn() + .mockResolvedValueOnce({ rows: marketRows }) + .mockResolvedValueOnce({ rows: [{ total: 17 }] }); + + const db: Queryable = { + query: (text: string, values?: unknown[]) => + queryMock(text, values) as Promise<{ rows: T[] }> + }; + + const result = await getMarkets( + { + filter: "active", + category: "Crypto", + sort: "volume", + page: 2, + limit: 10 + }, + db + ); + + expect(queryMock).toHaveBeenCalledTimes(2); + + const firstCall = queryMock.mock.calls[0]; + expect(firstCall[0]).toContain("FROM markets"); + expect(firstCall[0]).toContain("WHERE category = $1"); + expect(firstCall[0]).toContain("resolved = false"); + expect(firstCall[0]).toContain("ORDER BY (total_yes + total_no) DESC"); + expect(firstCall[1]).toEqual(["Crypto", 10, 10]); + + const secondCall = queryMock.mock.calls[1]; + expect(secondCall[0]).toContain("SELECT COUNT(*)::INT AS total"); + expect(secondCall[1]).toEqual(["Crypto"]); + + expect(result).toEqual({ + rows: marketRows, + total: 17, + page: 2, + limit: 10 + }); + }); +}); \ No newline at end of file diff --git a/backend/src/db/markets.ts b/backend/src/db/markets.ts new file mode 100644 index 0000000..662b178 --- /dev/null +++ b/backend/src/db/markets.ts @@ -0,0 +1,139 @@ +import { Pool } from "pg"; + +export type MarketFilter = "active" | "resolved" | "ended" | "cancelled" | "all"; +export type MarketSort = "newest" | "volume" | "ending_soon" | "bettors"; +export type MarketCategory = "Crypto" | "Sports" | "Politics" | "Entertainment" | "Science"; + +export type GetMarketsInput = { + filter?: MarketFilter; + category?: MarketCategory; + sort?: MarketSort; + page?: number; + limit?: number; +}; + +export type MarketRow = { + id: number; + question: string; + image_url: string | null; + category: string; + end_time: string; + total_yes: string; + total_no: string; + resolved: boolean; + outcome: boolean | null; + cancelled: boolean; + creator: string; + bet_count: number; + created_at: Date; + updated_at: Date; +}; + +export type GetMarketsResult = { + rows: MarketRow[]; + total: number; + page: number; + limit: number; +}; + +export type Queryable = { + query(text: string, values?: unknown[]): Promise<{ rows: T[] }>; +}; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + +const ORDER_BY: Record = { + newest: "created_at DESC", + volume: "(total_yes + total_no) DESC, created_at DESC", + ending_soon: "end_time ASC", + bettors: "bet_count DESC, created_at DESC" +}; + +function buildFilterClause(filter: MarketFilter): string { + switch (filter) { + case "active": + return "resolved = false AND cancelled = false AND end_time > EXTRACT(EPOCH FROM NOW())::BIGINT"; + case "resolved": + return "resolved = true"; + case "ended": + return "resolved = false AND cancelled = false AND end_time <= EXTRACT(EPOCH FROM NOW())::BIGINT"; + case "cancelled": + return "cancelled = true"; + case "all": + default: + return ""; + } +} + +export async function getMarkets( + { + filter = "all", + category, + sort = "newest", + page = 1, + limit = 20 + }: GetMarketsInput, + db: Queryable = pool +): Promise { + if (!Number.isInteger(page) || page < 1) { + throw new Error("page must be a positive integer"); + } + + if (!Number.isInteger(limit) || limit < 1) { + throw new Error("limit must be a positive integer"); + } + + const baseValues: unknown[] = []; + const whereConditions: string[] = []; + + if (category) { + baseValues.push(category); + whereConditions.push(`category = $${baseValues.length}`); + } + + const filterClause = buildFilterClause(filter); + if (filterClause) { + whereConditions.push(filterClause); + } + + const whereSql = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; + const offset = (page - 1) * limit; + + const rowsQuery = ` + SELECT + id, + question, + image_url, + category, + end_time, + total_yes, + total_no, + resolved, + outcome, + cancelled, + creator, + bet_count, + created_at, + updated_at + FROM markets + ${whereSql} + ORDER BY ${ORDER_BY[sort]} + LIMIT $${baseValues.length + 1} + OFFSET $${baseValues.length + 2} + `; + + const rowsValues = [...baseValues, limit, offset]; + const countQuery = `SELECT COUNT(*)::INT AS total FROM markets ${whereSql}`; + + const [{ rows }, { rows: totalRows }] = await Promise.all([ + db.query(rowsQuery, rowsValues), + db.query<{ total: number }>(countQuery, baseValues) + ]); + + return { + rows, + total: totalRows[0]?.total ?? 0, + page, + limit + }; +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..59fed3f --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,24 @@ +/** + * iPredict Backend API — entrypoint. + * + * This is an intentionally minimal scaffold. The real server bootstrap, + * routes, db/cache wiring, and config validation are tracked as separate + * issues (see GitHub issues labelled `area:backend`). + * + * Run: `npm run dev` + */ + +const PORT = Number(process.env.PORT ?? 4000); + +async function main(): Promise { + // TODO(#scaffold): replace with the Fastify server from `src/server.ts` + // once the "Bootstrap Fastify server" issue is implemented. + console.log(`[ipredict-backend] scaffold up — API server not yet implemented`); + console.log(`[ipredict-backend] intended port: ${PORT}`); + console.log(`[ipredict-backend] pick an issue labelled "area:backend" to start`); +} + +main().catch((err) => { + console.error("[ipredict-backend] fatal:", err); + process.exit(1); +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..c8962e9 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts new file mode 100644 index 0000000..6a4adb2 --- /dev/null +++ b/backend/vitest.config.ts @@ -0,0 +1,13 @@ +import path from "path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/db/README.md b/db/README.md new file mode 100644 index 0000000..8fcd15b --- /dev/null +++ b/db/README.md @@ -0,0 +1,58 @@ +# iPredict Database + +Shared PostgreSQL schema for the [`backend/`](../backend) and [`indexer/`](../indexer). +SQL migrations live in [`migrations/`](./migrations), applied in filename order. + +> **Branch:** all work happens on `implementation-drips`. + +## Schema overview + +See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#database-schema) +for the full reference. Core tables: + +- `markets` — indexed copy of on-chain markets +- `bets` — per-bettor positions per market +- `leaderboard` — points/win/loss snapshot +- `events` — raw on-chain event audit log +- (Phase 2) `oracle_submissions`, `oracle_disputes` + +## Migrations + +Each migration is a numbered SQL file: + +``` +migrations/ + 0001_create_markets.sql + 0002_create_bets.sql + ... +``` + +Apply with the migration runner (tracked as its own issue) or manually: + +```bash +psql "$DATABASE_URL" -f db/migrations/0001_create_markets.sql +``` + +## Local Seed Data + +Use the seed script to populate realistic local development records for +`markets`, `bets`, and `leaderboard` without running the full indexer. + +```bash +cd db +npm install +npm run seed +``` + +Defaults to `postgresql://ipredict:ipredict@localhost:5432/ipredict` if +`DATABASE_URL` is not set. + +The seed is idempotent: +- Tables are created with `CREATE TABLE IF NOT EXISTS` +- Records are inserted with `ON CONFLICT ... DO UPDATE` +- Safe to run multiple times + +## Contributing + +Pick an open issue labelled `area:db`, branch off `implementation-drips`, +PR back to `implementation-drips`. diff --git a/db/migrations/.gitkeep b/db/migrations/.gitkeep new file mode 100644 index 0000000..6950c57 --- /dev/null +++ b/db/migrations/.gitkeep @@ -0,0 +1 @@ +# Migrations go here, numbered 0001_*.sql, 0002_*.sql, ... (see ../README.md) diff --git a/db/package-lock.json b/db/package-lock.json new file mode 100644 index 0000000..3f31c8f --- /dev/null +++ b/db/package-lock.json @@ -0,0 +1,2162 @@ +{ + "name": "ipredict-db", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ipredict-db", + "version": "0.1.0", + "dependencies": { + "pg": "^8.12.0" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "@types/pg": "^8.11.6", + "tsx": "^4.19.0", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "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.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/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/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" + } + } + } +} diff --git a/db/package.json b/db/package.json new file mode 100644 index 0000000..e062c5f --- /dev/null +++ b/db/package.json @@ -0,0 +1,26 @@ +{ + "name": "ipredict-db", + "version": "0.1.0", + "private": true, + "description": "iPredict database utilities (migrations and local seed data)", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "seed": "tsx seed.ts", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "pg": "^8.12.0" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "@types/pg": "^8.11.6", + "tsx": "^4.19.0", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } +} \ No newline at end of file diff --git a/db/seed.test.ts b/db/seed.test.ts new file mode 100644 index 0000000..b8ec19e --- /dev/null +++ b/db/seed.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, vi } from "vitest"; + +import { runSeed } from "./seed"; + +describe("runSeed", () => { + it("uses transaction and conflict-safe inserts", async () => { + const query = vi.fn<(sql: string, values?: unknown[]) => Promise>( + async () => ({}) + ); + + await runSeed({ query }); + + expect(query).toHaveBeenCalled(); + expect(query.mock.calls[0]?.[0]).toBe("BEGIN"); + expect(query.mock.calls.at(-1)?.[0]).toBe("COMMIT"); + + const allSql = query.mock.calls + .map((call) => String(call[0])) + .join("\n"); + + expect(allSql).toContain("CREATE TABLE IF NOT EXISTS markets"); + expect(allSql).toContain("CREATE TABLE IF NOT EXISTS bets"); + expect(allSql).toContain("CREATE TABLE IF NOT EXISTS leaderboard"); + expect(allSql).toContain("ON CONFLICT (id) DO UPDATE"); + expect(allSql).toContain("ON CONFLICT (market_id, bettor) DO UPDATE"); + expect(allSql).toContain("ON CONFLICT (address) DO UPDATE"); + }); + + it("rolls back on error", async () => { + let call = 0; + const query = vi.fn<(sql: string, values?: unknown[]) => Promise>(async (sql: string) => { + call += 1; + if (call === 3) { + throw new Error("boom"); + } + return { sql }; + }); + + await expect(runSeed({ query })).rejects.toThrow("boom"); + expect(query).toHaveBeenCalledWith("ROLLBACK"); + }); +}); \ No newline at end of file diff --git a/db/seed.ts b/db/seed.ts new file mode 100644 index 0000000..e6c9964 --- /dev/null +++ b/db/seed.ts @@ -0,0 +1,335 @@ +import { Client } from "pg"; + +type Queryable = { + query(text: string, values?: unknown[]): Promise; +}; + +type SeedMarket = { + id: number; + question: string; + image_url: string | null; + category: "Crypto" | "Sports" | "Politics" | "Entertainment" | "Science"; + end_time: number; + total_yes: string; + total_no: string; + resolved: boolean; + outcome: boolean | null; + cancelled: boolean; + creator: string; + bet_count: number; +}; + +type SeedBet = { + market_id: number; + bettor: string; + net_amount: string; + gross_amount: string; + is_yes: boolean; + claimed: boolean; +}; + +type SeedLeaderboard = { + address: string; + display_name: string; + points: number; + won_bets: number; + lost_bets: number; +}; + +const SEED_MARKETS: SeedMarket[] = [ + { + id: 1, + question: "Will XLM close above $0.20 by Dec 31, 2026?", + image_url: null, + category: "Crypto", + end_time: 1798675200, + total_yes: "1200.0000000", + total_no: "800.0000000", + resolved: false, + outcome: null, + cancelled: false, + creator: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + bet_count: 3 + }, + { + id: 2, + question: "Will Team Alpha win the championship final?", + image_url: null, + category: "Sports", + end_time: 1788206400, + total_yes: "650.0000000", + total_no: "900.0000000", + resolved: false, + outcome: null, + cancelled: false, + creator: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + bet_count: 2 + }, + { + id: 3, + question: "Will Candidate Z win the 2026 election?", + image_url: null, + category: "Politics", + end_time: 1790966400, + total_yes: "1500.0000000", + total_no: "1400.0000000", + resolved: true, + outcome: true, + cancelled: false, + creator: "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + bet_count: 4 + } +]; + +const SEED_BETS: SeedBet[] = [ + { + market_id: 1, + bettor: "GUSER00000000000000000000000000000000000000000000000001", + net_amount: "350.0000000", + gross_amount: "357.0000000", + is_yes: true, + claimed: false + }, + { + market_id: 1, + bettor: "GUSER00000000000000000000000000000000000000000000000002", + net_amount: "500.0000000", + gross_amount: "510.0000000", + is_yes: false, + claimed: false + }, + { + market_id: 2, + bettor: "GUSER00000000000000000000000000000000000000000000000003", + net_amount: "400.0000000", + gross_amount: "408.0000000", + is_yes: true, + claimed: false + }, + { + market_id: 3, + bettor: "GUSER00000000000000000000000000000000000000000000000004", + net_amount: "900.0000000", + gross_amount: "918.0000000", + is_yes: true, + claimed: true + } +]; + +const SEED_LEADERBOARD: SeedLeaderboard[] = [ + { + address: "GUSER00000000000000000000000000000000000000000000000001", + display_name: "alpha_whale", + points: 120, + won_bets: 3, + lost_bets: 1 + }, + { + address: "GUSER00000000000000000000000000000000000000000000000002", + display_name: "beta_oracle", + points: 95, + won_bets: 2, + lost_bets: 2 + }, + { + address: "GUSER00000000000000000000000000000000000000000000000003", + display_name: "gamma_punter", + points: 70, + won_bets: 1, + lost_bets: 2 + } +]; + +async function ensureSchema(db: Queryable): Promise { + await db.query(` + CREATE TABLE IF NOT EXISTS markets ( + id BIGINT PRIMARY KEY, + question TEXT NOT NULL, + image_url TEXT, + category VARCHAR(20) NOT NULL, + end_time BIGINT NOT NULL, + total_yes NUMERIC(30,7) NOT NULL DEFAULT 0, + total_no NUMERIC(30,7) NOT NULL DEFAULT 0, + resolved BOOLEAN NOT NULL DEFAULT FALSE, + outcome BOOLEAN, + cancelled BOOLEAN NOT NULL DEFAULT FALSE, + creator CHAR(56) NOT NULL, + bet_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + + await db.query(` + CREATE TABLE IF NOT EXISTS bets ( + market_id BIGINT REFERENCES markets(id), + bettor CHAR(56) NOT NULL, + net_amount NUMERIC(30,7) NOT NULL, + gross_amount NUMERIC(30,7) NOT NULL, + is_yes BOOLEAN NOT NULL, + claimed BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (market_id, bettor) + ) + `); + + await db.query(` + CREATE TABLE IF NOT EXISTS leaderboard ( + address CHAR(56) PRIMARY KEY, + display_name VARCHAR(50), + points BIGINT NOT NULL DEFAULT 0, + won_bets INTEGER NOT NULL DEFAULT 0, + lost_bets INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function seedMarkets(db: Queryable): Promise { + const query = ` + INSERT INTO markets ( + id, + question, + image_url, + category, + end_time, + total_yes, + total_no, + resolved, + outcome, + cancelled, + creator, + bet_count, + updated_at + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW() + ) + ON CONFLICT (id) DO UPDATE SET + question = EXCLUDED.question, + image_url = EXCLUDED.image_url, + category = EXCLUDED.category, + end_time = EXCLUDED.end_time, + total_yes = EXCLUDED.total_yes, + total_no = EXCLUDED.total_no, + resolved = EXCLUDED.resolved, + outcome = EXCLUDED.outcome, + cancelled = EXCLUDED.cancelled, + creator = EXCLUDED.creator, + bet_count = EXCLUDED.bet_count, + updated_at = NOW() + `; + + for (const market of SEED_MARKETS) { + await db.query(query, [ + market.id, + market.question, + market.image_url, + market.category, + market.end_time, + market.total_yes, + market.total_no, + market.resolved, + market.outcome, + market.cancelled, + market.creator, + market.bet_count + ]); + } +} + +async function seedBets(db: Queryable): Promise { + const query = ` + INSERT INTO bets ( + market_id, + bettor, + net_amount, + gross_amount, + is_yes, + claimed + ) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (market_id, bettor) DO UPDATE SET + net_amount = EXCLUDED.net_amount, + gross_amount = EXCLUDED.gross_amount, + is_yes = EXCLUDED.is_yes, + claimed = EXCLUDED.claimed + `; + + for (const bet of SEED_BETS) { + await db.query(query, [ + bet.market_id, + bet.bettor, + bet.net_amount, + bet.gross_amount, + bet.is_yes, + bet.claimed + ]); + } +} + +async function seedLeaderboard(db: Queryable): Promise { + const query = ` + INSERT INTO leaderboard ( + address, + display_name, + points, + won_bets, + lost_bets, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (address) DO UPDATE SET + display_name = EXCLUDED.display_name, + points = EXCLUDED.points, + won_bets = EXCLUDED.won_bets, + lost_bets = EXCLUDED.lost_bets, + updated_at = NOW() + `; + + for (const row of SEED_LEADERBOARD) { + await db.query(query, [ + row.address, + row.display_name, + row.points, + row.won_bets, + row.lost_bets + ]); + } +} + +export async function runSeed(db: Queryable): Promise { + await db.query("BEGIN"); + try { + await ensureSchema(db); + await seedMarkets(db); + await seedBets(db); + await seedLeaderboard(db); + await db.query("COMMIT"); + } catch (error) { + await db.query("ROLLBACK"); + throw error; + } +} + +async function main(): Promise { + const connectionString = + process.env.DATABASE_URL ?? "postgresql://ipredict:ipredict@localhost:5432/ipredict"; + + const client = new Client({ connectionString }); + await client.connect(); + + try { + await runSeed(client); + console.log("[ipredict-db] seed completed successfully"); + } finally { + await client.end(); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + console.error("[ipredict-db] seed failed", error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/db/tsconfig.json b/db/tsconfig.json new file mode 100644 index 0000000..ab0bbc0 --- /dev/null +++ b/db/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "vitest/globals"], + "noEmit": true + }, + "include": ["*.ts"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/docs/ORACLE_AND_BACKEND.md b/docs/ORACLE_AND_BACKEND.md index 4d2f7de..f1ae68b 100644 --- a/docs/ORACLE_AND_BACKEND.md +++ b/docs/ORACLE_AND_BACKEND.md @@ -359,6 +359,21 @@ while (true) { } ``` +### Leaderboard Rebuild Job + +When the `events` table needs a clean backfill, rebuild the leaderboard snapshot +from the raw event log with the standalone job in `indexer/src/`. + +Runbook: + +1. Export `DATABASE_URL` for the Postgres database that stores `events` and `leaderboard`. +2. From `indexer/`, run `npm run rebuild:leaderboard`. +3. Use `npm run rebuild:leaderboard -- --dry-run` first if you want to validate the replay without mutating the table. +4. If you only want to replay from a specific ledger onward, pass `--since-ledger `. + +The job replays claim and referral-style events, clears `leaderboard`, and +re-inserts the derived rows in score order. + --- ### API Endpoints diff --git a/indexer/.env.example b/indexer/.env.example new file mode 100644 index 0000000..21ce438 --- /dev/null +++ b/indexer/.env.example @@ -0,0 +1,25 @@ +# ── Indexer configuration ───────────────────────────────────────────────────── +# Copy to .env and fill in real values. + +# PostgreSQL (shared with the backend) +DATABASE_URL=postgres://ipredict:ipredict@localhost:5432/ipredict + +# Redis (for cache invalidation) +REDIS_URL=redis://localhost:6379 + +# Stellar / Soroban +SOROBAN_RPC_URL=https://mainnet.sorobanrpc.com +NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015 + +# Contract IDs to index (from deploy-mainnet-output.json) +MARKET_CONTRACT_ID= +TOKEN_CONTRACT_ID= +REFERRAL_CONTRACT_ID= +LEADERBOARD_CONTRACT_ID= + +# Polling +POLL_INTERVAL_MS=5000 +EVENTS_PER_PAGE=200 + +# Ledger to start indexing from if no checkpoint exists (0 = earliest available) +START_LEDGER=0 diff --git a/indexer/README.md b/indexer/README.md new file mode 100644 index 0000000..311e601 --- /dev/null +++ b/indexer/README.md @@ -0,0 +1,61 @@ +# iPredict Soroban Event Indexer + +Polls the Soroban RPC `getEvents()` endpoint, decodes contract events +(markets created, bets placed, claims, resolutions, oracle submissions…), and +writes them into PostgreSQL so the [`backend/`](../backend) can serve fast, +indexed reads. Also invalidates the Redis cache on relevant updates. + +> **Branch:** all work happens on `implementation-drips`. Open PRs against that +> branch, **not** `main`. + +## Stack + +- **Runtime:** Node.js 20+, TypeScript +- **DB:** PostgreSQL 16 (shared with the backend) +- **Cache:** Redis 7 (invalidation only) +- **Stellar:** `@stellar/stellar-sdk` + +## How it works + +``` +loop every 5s: + events = rpc.getEvents({ startLedger: checkpoint, contractIds: [...] }) + for each event: + decode topics + value (scValToNative) + upsert into markets / bets / leaderboard / events tables + invalidate affected Redis keys + save checkpoint = latestLedger +``` + +See [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#soroban-event-indexer) +for the reference implementation and the DB schema. + +## Layout + +``` +indexer/ + src/ + handlers/ one decoder per event type (market_created, bet, claim, ...) + db/ write queries + checkpoint store + rpc/ getEvents client + pagination + config/ env loading + index.ts polling loop entrypoint + test/ + package.json + tsconfig.json + .env.example +``` + +## Getting started + +```bash +cd indexer +cp .env.example .env # fill in DATABASE_URL, SOROBAN_RPC_URL, contract IDs +npm install +npm run dev +``` + +## Contributing + +Pick an open issue labelled `area:indexer`, claim it, branch off +`implementation-drips`, and PR back to `implementation-drips`. diff --git a/indexer/package-lock.json b/indexer/package-lock.json new file mode 100644 index 0000000..7ac3677 --- /dev/null +++ b/indexer/package-lock.json @@ -0,0 +1,2159 @@ +{ + "name": "ipredict-indexer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ipredict-indexer", + "version": "0.1.0", + "dependencies": { + "pg": "^8.13.3" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "@types/pg": "^8.11.11", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "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.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/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/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" + } + } + } +} diff --git a/indexer/package.json b/indexer/package.json new file mode 100644 index 0000000..ac92a64 --- /dev/null +++ b/indexer/package.json @@ -0,0 +1,43 @@ +{ + "name": "ipredict-indexer", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run", + "rebuild:leaderboard": "tsx src/index.ts" + }, + "dependencies": { + "pg": "^8.13.3" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "@types/pg": "^8.11.11", + "tsx": "^4.19.2", + "description": "iPredict Soroban event indexer — writes on-chain events to PostgreSQL", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@stellar/stellar-sdk": "^14.5.0", + "ioredis": "^5.4.1", + "pg": "^8.12.0" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "@types/pg": "^8.11.6", + "tsx": "^4.19.0", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } +} diff --git a/indexer/src/__tests__/leaderboard-rebuild.test.ts b/indexer/src/__tests__/leaderboard-rebuild.test.ts new file mode 100644 index 0000000..7325b26 --- /dev/null +++ b/indexer/src/__tests__/leaderboard-rebuild.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it, vi } from "vitest"; +import { buildLeaderboardSnapshot, rebuildLeaderboardTable } from "../leaderboard-rebuild.js"; + +describe("buildLeaderboardSnapshot", () => { + it("replays claim and referral events into a sorted leaderboard snapshot", () => { + const snapshot = buildLeaderboardSnapshot([ + { + id: 1, + ledgerSeq: 10, + eventType: "referral_registered", + marketId: null, + actor: "GUSER1", + payload: { + display_name: "Ada", + referrer: "GREF", + }, + }, + { + id: 2, + ledgerSeq: 11, + eventType: "reward_claimed", + marketId: 7, + actor: null, + payload: { + user: "GUSER1", + is_winner: true, + points: 30, + }, + }, + { + id: 3, + ledgerSeq: 12, + eventType: "referral_credited", + marketId: 7, + actor: "GUSER1", + payload: { + referrer: "GREF", + bonus_points: 3, + }, + }, + { + id: 4, + ledgerSeq: 13, + eventType: "reward_claimed", + marketId: 8, + actor: null, + payload: { + user: "GUSER2", + is_winner: false, + }, + }, + ]); + + expect(snapshot.eventCount).toBe(4); + expect(snapshot.lastLedgerSeq).toBe(13); + expect(snapshot.players).toEqual([ + { + address: "GUSER1", + displayName: "Ada", + points: 35, + wonBets: 1, + lostBets: 0, + }, + { + address: "GUSER2", + displayName: "", + points: 10, + wonBets: 0, + lostBets: 1, + }, + { + address: "GREF", + displayName: "", + points: 8, + wonBets: 0, + lostBets: 0, + }, + ]); + }); +}); + +describe("rebuildLeaderboardTable", () => { + it("loads events, clears the leaderboard, and inserts the rebuilt rows", async () => { + const queries: Array<{ text: string; params?: readonly unknown[] }> = []; + const db = { + query: vi.fn(async (text: string, params?: readonly unknown[]) => { + queries.push({ text, params }); + + if (text.startsWith("SELECT id, ledger_seq")) { + return { + rows: [ + { + id: 1, + ledger_seq: 1, + event_type: "reward_claimed", + market_id: 1, + actor: null, + payload: { user: "GALICE", is_winner: true, points: 30 }, + }, + { + id: 2, + ledger_seq: 2, + event_type: "referral_credited", + market_id: 1, + actor: "GALICE", + payload: { referrer: "GBOB", bonus_points: 3 }, + }, + ], + }; + } + + return { rows: [] }; + }), + }; + + const snapshot = await rebuildLeaderboardTable(db); + + expect(snapshot.players).toEqual([ + { + address: "GALICE", + displayName: "", + points: 30, + wonBets: 1, + lostBets: 0, + }, + { + address: "GBOB", + displayName: "", + points: 3, + wonBets: 0, + lostBets: 0, + }, + ]); + + expect(queries[0]?.text).toContain("FROM events"); + expect(queries[1]?.text).toBe("DELETE FROM leaderboard"); + expect(queries[2]?.text).toContain("INSERT INTO leaderboard"); + expect(queries[2]?.params).toEqual([ + "GALICE", + null, + 30, + 1, + 0, + "GBOB", + null, + 3, + 0, + 0, + ]); + }); +}); diff --git a/indexer/src/index.ts b/indexer/src/index.ts new file mode 100644 index 0000000..11944f9 --- /dev/null +++ b/indexer/src/index.ts @@ -0,0 +1,91 @@ +import { Pool } from "pg"; +import { rebuildLeaderboardTable } from "./leaderboard-rebuild.js"; + +function parseSinceLedger(argv: string[]): number | undefined { + const exact = argv.find((arg) => arg.startsWith("--since-ledger=")); + if (exact) { + const value = Number(exact.split("=", 2)[1]); + return Number.isFinite(value) && value >= 0 ? value : undefined; + } + + const index = argv.indexOf("--since-ledger"); + if (index >= 0 && argv[index + 1]) { + const value = Number(argv[index + 1]); + return Number.isFinite(value) && value >= 0 ? value : undefined; + } + + return undefined; +} + +async function main(): Promise { + const connectionString = process.env.DATABASE_URL; + if (!connectionString) { + throw new Error("DATABASE_URL is required to rebuild the leaderboard"); + } + + const dryRun = process.argv.includes("--dry-run"); + const sinceLedger = parseSinceLedger(process.argv.slice(2)); + const pool = new Pool({ connectionString }); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + const snapshot = await rebuildLeaderboardTable(client, { + dryRun, + sinceLedger, + }); + + if (dryRun) { + await client.query("ROLLBACK"); + } else { + await client.query("COMMIT"); + } + + const summary = [ + `processed ${snapshot.eventCount} event(s)`, + `rebuilt ${snapshot.players.length} leaderboard row(s)`, + snapshot.lastLedgerSeq === null ? null : `last ledger ${snapshot.lastLedgerSeq}`, + sinceLedger === undefined ? null : `checkpoint hint ${sinceLedger}`, + dryRun ? "dry-run" : null, + ] + .filter((part): part is string => part !== null) + .join(", "); + + console.log(`[leaderboard-rebuild] ${summary}`); + } catch (error) { + await client.query("ROLLBACK").catch(() => undefined); + throw error; + } finally { + client.release(); + await pool.end(); + } +} + +main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`[leaderboard-rebuild] ${message}`); + process.exitCode = 1; +/** + * iPredict Soroban Event Indexer — entrypoint. + * + * Minimal scaffold. The real polling loop, getEvents client, event decoders, + * checkpoint store, and DB writers are tracked as separate issues + * (see GitHub issues labelled `area:indexer`). + * + * Run: `npm run dev` + */ + +const POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS ?? 5000); + +async function main(): Promise { + // TODO(#scaffold): replace with the real polling loop once the + // "Implement getEvents polling loop" issue is done. + console.log(`[ipredict-indexer] scaffold up — polling loop not yet implemented`); + console.log(`[ipredict-indexer] intended interval: ${POLL_INTERVAL_MS}ms`); + console.log(`[ipredict-indexer] pick an issue labelled "area:indexer" to start`); +} + +main().catch((err) => { + console.error("[ipredict-indexer] fatal:", err); + process.exit(1); +}); diff --git a/indexer/src/leaderboard-rebuild.ts b/indexer/src/leaderboard-rebuild.ts new file mode 100644 index 0000000..13c1c56 --- /dev/null +++ b/indexer/src/leaderboard-rebuild.ts @@ -0,0 +1,352 @@ +export interface EventLogRow { + id: number; + ledgerSeq: number; + eventType: string; + marketId: number | null; + actor: string | null; + payload: unknown; +} + +export interface LeaderboardRow { + address: string; + displayName: string; + points: number; + wonBets: number; + lostBets: number; +} + +export interface LeaderboardSnapshot { + players: LeaderboardRow[]; + eventCount: number; + lastLedgerSeq: number | null; +} + +export interface Queryable { + query( + text: string, + params?: readonly unknown[] + ): Promise<{ rows: any[] }>; +} + +export interface RebuildOptions { + dryRun?: boolean; + sinceLedger?: number; +} + +interface PlayerState extends LeaderboardRow { + lastTouchedLedger: number; +} + +interface EventRowRecord { + id: number | string; + ledger_seq: number | string; + event_type: string; + market_id: number | string | null; + actor: string | null; + payload: unknown; +} + +const CLAIM_EVENT_TYPES = new Set([ + "reward_claimed", + "market_reward_claimed", + "claim_rewarded", +]); + +const REFERRAL_REGISTRATION_TYPES = new Set([ + "referral_registered", + "registered_referral", +]); + +const REFERRAL_BONUS_TYPES = new Set([ + "referral_credited", + "referral_bonus", + "bonus_points_awarded", + "leaderboard_bonus", +]); + +function asRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +function toStringOrNull(value: unknown): string | null { + if (typeof value === "string" && value.trim().length > 0) return value.trim(); + if (typeof value === "number" || typeof value === "bigint") return String(value); + return null; +} + +function toNumberOrNull(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "bigint") return Number(value); + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +function toBooleanOrNull(value: unknown): boolean | null { + if (typeof value === "boolean") return value; + if (typeof value === "number") return value !== 0; + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (["true", "t", "yes", "y", "win", "winner", "won", "1"].includes(normalized)) { + return true; + } + if (["false", "f", "no", "n", "lose", "loser", "lost", "0"].includes(normalized)) { + return false; + } + } + return null; +} + +function firstString(source: Record, keys: string[]): string | null { + for (const key of keys) { + const value = toStringOrNull(source[key]); + if (value !== null) return value; + } + return null; +} + +function firstNumber(source: Record, keys: string[]): number | null { + for (const key of keys) { + const value = toNumberOrNull(source[key]); + if (value !== null) return value; + } + return null; +} + +function firstBoolean(source: Record, keys: string[]): boolean | null { + for (const key of keys) { + const value = toBooleanOrNull(source[key]); + if (value !== null) return value; + } + return null; +} + +function looksLikeClaimEvent(eventType: string, payload: Record): boolean { + if (CLAIM_EVENT_TYPES.has(eventType)) return true; + return ( + eventType.includes("claim") || + (firstBoolean(payload, ["is_winner", "isWinner", "won"]) !== null && + firstNumber(payload, ["points", "points_delta", "reward_points"]) !== null) + ); +} + +function getPlayer( + players: Map, + address: string, + ledgerSeq: number +): PlayerState { + const existing = players.get(address); + if (existing) { + existing.lastTouchedLedger = Math.max(existing.lastTouchedLedger, ledgerSeq); + return existing; + } + + const created: PlayerState = { + address, + displayName: "", + points: 0, + wonBets: 0, + lostBets: 0, + lastTouchedLedger: ledgerSeq, + }; + players.set(address, created); + return created; +} + +function applyPointsDelta(player: PlayerState, delta: number): void { + player.points += delta; +} + +function handleClaimEvent( + players: Map, + event: EventLogRow, + payload: Record +): void { + const user = + firstString(payload, ["user", "address", "bettor", "recipient"]) ?? event.actor; + if (!user) return; + + const player = getPlayer(players, user, event.ledgerSeq); + const displayName = firstString(payload, ["display_name", "displayName", "name"]); + if (displayName) player.displayName = displayName; + + const isWinner = firstBoolean(payload, ["is_winner", "isWinner", "won"]) ?? false; + const points = + firstNumber(payload, ["points", "points_delta", "reward_points"]) ?? + (isWinner ? 30 : 10); + + applyPointsDelta(player, points); + if (isWinner) { + player.wonBets += 1; + } else { + player.lostBets += 1; + } +} + +function handleReferralRegistration( + players: Map, + event: EventLogRow, + payload: Record +): void { + const user = + firstString(payload, ["user", "address", "registrant"]) ?? event.actor; + if (!user) return; + + const userPlayer = getPlayer(players, user, event.ledgerSeq); + const displayName = firstString(payload, ["display_name", "displayName", "name"]); + if (displayName) userPlayer.displayName = displayName; + + const welcomeBonus = firstNumber(payload, ["welcome_bonus_points", "points"]) ?? 5; + applyPointsDelta(userPlayer, welcomeBonus); + + const referrer = firstString(payload, ["referrer", "referrer_address"]); + if (!referrer) return; + + const referrerBonus = firstNumber( + payload, + ["referrer_bonus_points", "bonus_points", "referral_points"] + ) ?? 5; + applyPointsDelta(getPlayer(players, referrer, event.ledgerSeq), referrerBonus); +} + +function handleReferralBonus( + players: Map, + event: EventLogRow, + payload: Record +): void { + const target = + firstString(payload, ["referrer", "user", "address", "recipient"]) ?? event.actor; + if (!target) return; + + const player = getPlayer(players, target, event.ledgerSeq); + const displayName = firstString(payload, ["display_name", "displayName", "name"]); + if (displayName) player.displayName = displayName; + + const delta = firstNumber(payload, ["points", "bonus_points", "points_delta"]) ?? 3; + applyPointsDelta(player, delta); +} + +export function buildLeaderboardSnapshot(events: EventLogRow[]): LeaderboardSnapshot { + const players = new Map(); + let eventCount = 0; + let lastLedgerSeq: number | null = null; + + for (const event of events) { + eventCount += 1; + lastLedgerSeq = event.ledgerSeq; + const payload = asRecord(event.payload); + + if (looksLikeClaimEvent(event.eventType, payload)) { + handleClaimEvent(players, event, payload); + continue; + } + + if (REFERRAL_REGISTRATION_TYPES.has(event.eventType) || event.eventType.includes("register")) { + handleReferralRegistration(players, event, payload); + continue; + } + + if (REFERRAL_BONUS_TYPES.has(event.eventType) || event.eventType.includes("referral")) { + handleReferralBonus(players, event, payload); + continue; + } + + const target = + firstString(payload, ["user", "address", "actor", "recipient"]) ?? event.actor; + const delta = firstNumber(payload, ["points", "points_delta", "bonus_points"]); + if (!target || delta === null) continue; + + const player = getPlayer(players, target, event.ledgerSeq); + const displayName = firstString(payload, ["display_name", "displayName", "name"]); + if (displayName) player.displayName = displayName; + applyPointsDelta(player, delta); + } + + const snapshotPlayers = [...players.values()] + .sort((a, b) => { + if (b.points !== a.points) return b.points - a.points; + if (b.wonBets !== a.wonBets) return b.wonBets - a.wonBets; + if (a.lostBets !== b.lostBets) return a.lostBets - b.lostBets; + return a.address.localeCompare(b.address); + }) + .map(({ lastTouchedLedger: _lastTouchedLedger, ...row }) => row); + + return { + players: snapshotPlayers, + eventCount, + lastLedgerSeq, + }; +} + +export async function rebuildLeaderboardTable( + db: Queryable, + options: RebuildOptions = {} +): Promise { + const queryParts = [ + "SELECT id, ledger_seq, event_type, market_id, actor, payload", + "FROM events", + ]; + const queryParams: unknown[] = []; + + if (options.sinceLedger !== undefined) { + queryParams.push(options.sinceLedger); + queryParts.push(`WHERE ledger_seq >= $${queryParams.length}`); + } + + queryParts.push("ORDER BY ledger_seq ASC, id ASC"); + + const { rows } = await db.query(queryParts.join(" "), queryParams); + + const events = rows.map((row) => ({ + id: Number(row.id), + ledgerSeq: Number(row.ledger_seq), + eventType: String(row.event_type), + marketId: row.market_id === null ? null : Number(row.market_id), + actor: row.actor, + payload: row.payload, + })); + + const snapshot = buildLeaderboardSnapshot(events); + if (options.dryRun) { + return snapshot; + } + + await db.query("DELETE FROM leaderboard"); + + if (snapshot.players.length === 0) { + return snapshot; + } + + const values: unknown[] = []; + const placeholders = snapshot.players.map((player, index) => { + const offset = index * 5; + values.push( + player.address, + player.displayName || null, + player.points, + player.wonBets, + player.lostBets + ); + return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, NOW())`; + }); + + await db.query( + [ + "INSERT INTO leaderboard (address, display_name, points, won_bets, lost_bets, updated_at)", + `VALUES ${placeholders.join(", ")}`, + "ON CONFLICT (address) DO UPDATE SET", + " display_name = EXCLUDED.display_name,", + " points = EXCLUDED.points,", + " won_bets = EXCLUDED.won_bets,", + " lost_bets = EXCLUDED.lost_bets,", + " updated_at = NOW()", + ].join(" "), + values + ); + + return snapshot; +} diff --git a/indexer/tsconfig.json b/indexer/tsconfig.json new file mode 100644 index 0000000..13a95ef --- /dev/null +++ b/indexer/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src"] + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/indexer/vitest.config.ts b/indexer/vitest.config.ts new file mode 100644 index 0000000..f624398 --- /dev/null +++ b/indexer/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +}); diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..23043fe --- /dev/null +++ b/infra/README.md @@ -0,0 +1,33 @@ +# iPredict Infrastructure + +Local-dev and production infrastructure for the backend stack: Postgres, Redis, +the API service, the indexer, and the oracle services. + +> **Branch:** all work happens on `implementation-drips`. + +## Local development + +Start Postgres + Redis (enough to run the backend and indexer locally): + +```bash +cd infra +docker compose -f docker-compose.dev.yml up -d +``` + +This gives you: +- Postgres on `localhost:5432` (db `ipredict`, user/pass `ipredict`) +- Redis on `localhost:6379` + +Then run each service from its own folder (`backend/`, `indexer/`, `oracle/`) +with `npm run dev`. + +## Production + +`docker-compose.production.yml` (tracked as its own issue) builds and runs the +API (replicated), the single indexer, Postgres, and Redis. See +[`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#infrastructure). + +## Contributing + +Pick an open issue labelled `area:infra`, branch off `implementation-drips`, +PR back to `implementation-drips`. diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml new file mode 100644 index 0000000..b7831a6 --- /dev/null +++ b/infra/docker-compose.dev.yml @@ -0,0 +1,36 @@ +# Local development infrastructure: Postgres + Redis only. +# The app services (api, indexer, oracle) run on the host via `npm run dev` +# during development. See docker-compose.production.yml for the full stack. + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: ipredict + POSTGRES_PASSWORD: ipredict + POSTGRES_DB: ipredict + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ipredict"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redisdata:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: + redisdata: diff --git a/oracle/.env.example b/oracle/.env.example new file mode 100644 index 0000000..4e16f87 --- /dev/null +++ b/oracle/.env.example @@ -0,0 +1,28 @@ +# ── Oracle configuration ────────────────────────────────────────────────────── +# Copy to .env and fill in real values. + +# Stellar / Soroban +SOROBAN_RPC_URL=https://mainnet.sorobanrpc.com +NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015 +MARKET_CONTRACT_ID= + +# Oracle provider signing key (KEEP SECRET — never commit a real one) +ORACLE_SECRET_KEY= + +# Council aggregator +COUNCIL_THRESHOLD=4 +COUNCIL_SIZE=7 + +# Optimistic oracle (Phase 2) +SUBMITTER_BOND_XLM=100 +DISPUTER_BOND_XLM=200 + +# Data source API keys +COINGECKO_API_KEY= +BINANCE_API_KEY= +SPORTS_API_KEY= +ODDS_API_KEY= + +# Monitoring / alerting +ALERT_WEBHOOK_URL= +MARKET_UNRESOLVED_ALERT_HOURS=6 diff --git a/oracle/README.md b/oracle/README.md new file mode 100644 index 0000000..c20ae67 --- /dev/null +++ b/oracle/README.md @@ -0,0 +1,59 @@ +# iPredict Oracle + +The resolution layer that decides market outcomes without trusting a single +admin. Built in phases (see [`docs/ORACLE_AND_BACKEND.md`](../docs/ORACLE_AND_BACKEND.md#part-1--oracle-architecture)): + +| Phase | Model | Contract changes | +|---|---|---| +| **1.5** | 4-of-7 Council multisig + off-chain aggregator | none (uses existing `add_resolver`/`resolve_market`) | +| **2** | Optimistic oracle — bonded submissions, challenge window, dispute council | new contract functions & state | +| **Data** | Off-chain data adapters (CoinGecko, Binance, sports, politics) feeding submitters | none | + +> **Branch:** all work happens on `implementation-drips`. Open PRs against that +> branch, **not** `main`. + +## What lives here + +- **`aggregator/`** — off-chain service that watches council submissions and + fires the final on-chain resolution once threshold is met. +- **`adapters/`** — one module per data source; each returns a normalized + outcome for a given market. +- **`submitter/`** — signs and submits outcomes/bonds on behalf of an oracle + provider. +- **`monitor/`** — watches oracle events (submissions, challenges, disputes) + and alerts on stuck/conflicting markets. + +> Note: the *contract-side* oracle changes (optimistic oracle state machine, +> bonds, dispute council) live under [`contracts/`](../contracts) as new Soroban +> code, tracked by issues labelled `area:oracle` + `area:contracts`. + +## Layout + +``` +oracle/ + src/ + aggregator/ + adapters/ coingecko.ts, binance.ts, sports.ts, politics.ts, ... + submitter/ + monitor/ + config/ + index.ts + test/ + package.json + tsconfig.json + .env.example +``` + +## Getting started + +```bash +cd oracle +cp .env.example .env +npm install +npm run dev +``` + +## Contributing + +Pick an open issue labelled `area:oracle`, claim it, branch off +`implementation-drips`, PR back to `implementation-drips`. diff --git a/oracle/package.json b/oracle/package.json new file mode 100644 index 0000000..d6e09fb --- /dev/null +++ b/oracle/package.json @@ -0,0 +1,28 @@ +{ + "name": "ipredict-oracle", + "version": "0.1.0", + "private": true, + "description": "iPredict oracle — council aggregator, data adapters, submitter, monitor", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@stellar/stellar-sdk": "^14.5.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.13.5", + "tsx": "^4.19.0", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } +} diff --git a/oracle/src/index.ts b/oracle/src/index.ts new file mode 100644 index 0000000..c12e2d2 --- /dev/null +++ b/oracle/src/index.ts @@ -0,0 +1,20 @@ +/** + * iPredict Oracle — entrypoint. + * + * Minimal scaffold. The council aggregator, data adapters, submitter, and + * monitor are tracked as separate issues (see GitHub issues labelled + * `area:oracle`). + * + * Run: `npm run dev` + */ + +async function main(): Promise { + // TODO(#scaffold): wire up the aggregator / monitor once their issues land. + console.log(`[ipredict-oracle] scaffold up — oracle services not yet implemented`); + console.log(`[ipredict-oracle] pick an issue labelled "area:oracle" to start`); +} + +main().catch((err) => { + console.error("[ipredict-oracle] fatal:", err); + process.exit(1); +}); diff --git a/oracle/tsconfig.json b/oracle/tsconfig.json new file mode 100644 index 0000000..b796c6f --- /dev/null +++ b/oracle/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/scripts/create-issues.sh b/scripts/create-issues.sh new file mode 100644 index 0000000..158ef65 --- /dev/null +++ b/scripts/create-issues.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Create iPredict backend/oracle issues from a TSV file. +# +# TSV columns (tab-separated), one issue per line: +# title \t labels(comma-sep) \t milestone-number \t body-file-path +# +# Usage: scripts/create-issues.sh +# +# Idempotency: skips creating an issue whose exact title already exists (open or closed). +set -euo pipefail + +REPO="Akanimoh12/Stellar-iPredict" +TSV="${1:?usage: create-issues.sh }" + +# Cache existing titles once to avoid an API call per issue. +echo "Fetching existing issue titles…" +EXISTING="$(gh issue list --repo "$REPO" --state all --limit 1000 --json title --jq '.[].title')" + +created=0 +skipped=0 +while IFS=$'\t' read -r title labels milestone bodyfile; do + [ -z "${title:-}" ] && continue + case "$title" in \#*) continue;; esac # allow # comments + + if grep -Fxq "$title" <<<"$EXISTING"; then + echo "skip (exists): $title" + skipped=$((skipped+1)) + continue + fi + + gh issue create --repo "$REPO" \ + --title "$title" \ + --label "$labels" \ + --milestone "$milestone" \ + --body-file "$bodyfile" >/dev/null + + echo "created: $title" + created=$((created+1)) + sleep 0.6 # gentle on the API +done < "$TSV" + +echo "----" +echo "created=$created skipped=$skipped" diff --git a/scripts/gen_issues.py b/scripts/gen_issues.py new file mode 100644 index 0000000..43cf76d --- /dev/null +++ b/scripts/gen_issues.py @@ -0,0 +1,1069 @@ +#!/usr/bin/env python3 +""" +Generate and create the 260 iPredict backend/oracle implementation issues. + +Each issue gets a consistent, detailed body: + - Context (why this matters) + - What to build (concrete scope) + - Acceptance criteria (checklist) + - Files / location + - PR target reminder (implementation-drips) + +Usage: + python3 scripts/gen_issues.py --dry-run # print titles + counts only + python3 scripts/gen_issues.py --sample 12 # create only the first N (review) + python3 scripts/gen_issues.py # create ALL (skips existing titles) +""" +import argparse +import json +import subprocess +import sys +import time + +REPO = "Akanimoh12/Stellar-iPredict" +BRANCH = "implementation-drips" + +# Milestone TITLES (gh issue create --milestone takes the title, not the number) +MS_COUNCIL = "Phase 1.5 — Council Resolution" +MS_BACKEND = "Backend MVP — API + DB + Indexer" +MS_CACHE = "Caching & Performance" +MS_OPTIMISTIC = "Phase 2 — Optimistic Oracle" +MS_DATA = "Oracle Data Layer" +MS_INFRA = "Infrastructure & Deployment" +MS_MONITOR = "Monitoring & Alerting" +MS_TESTING = "Testing & Hardening" + + +def body(context, what, criteria, location, extra=""): + crit = "\n".join(f"- [ ] {c}" for c in criteria) + extra_block = f"\n## Notes\n{extra}\n" if extra else "" + return f"""## Context +{context} + +## What to build +{what} + +## Acceptance criteria +{crit} + +## Files / location +`{location}` +{extra_block} +--- +**PR target:** open your PR against the `{BRANCH}` branch (not `main`). +Reference: [`docs/ORACLE_AND_BACKEND.md`](../blob/{BRANCH}/docs/ORACLE_AND_BACKEND.md). +There is no CI on this branch yet — run `npm run typecheck` and `npm test` locally before review. +""" + + +# Each entry: (title, [labels], milestone, body) +ISSUES = [] + + +def add(title, labels, ms, context, what, criteria, location, extra=""): + ISSUES.append((title, labels, ms, body(context, what, criteria, location, extra))) + + +# ────────────────────────────────────────────────────────────────────────────── +# DATABASE (area:db) — ~30 +# ────────────────────────────────────────────────────────────────────────────── +add("[DB] Create `markets` table migration", + ["area:db", "P0"], MS_BACKEND, + "The backend serves an indexed copy of on-chain markets. We need the base table.", + "Write migration `0001_create_markets.sql` matching the schema in the design doc " + "(id, question, image_url, category, end_time, total_yes, total_no, resolved, " + "outcome, cancelled, creator, bet_count, created_at, updated_at).", + ["Migration file `db/migrations/0001_create_markets.sql` exists", + "Column types match the design doc (NUMERIC(30,7) for amounts, CHAR(56) for addresses)", + "Running it against a clean Postgres creates the table without error", + "Includes `IF NOT EXISTS` / is safely re-runnable or documented as forward-only"], + "db/migrations/0001_create_markets.sql") + +add("[DB] Add indexes on `markets` (category, resolved, active)", + ["area:db", "P0"], MS_BACKEND, + "Market list queries filter by category and active/resolved state; these need indexes.", + "Add `idx_markets_category`, `idx_markets_resolved(resolved,end_time)`, and " + "`idx_markets_active(resolved,cancelled,end_time)`.", + ["Three indexes created in a migration", + "EXPLAIN on a filtered `SELECT ... WHERE resolved=false AND cancelled=false` uses the index"], + "db/migrations/0002_markets_indexes.sql") + +add("[DB] Create `bets` table migration", + ["area:db", "P0"], MS_BACKEND, + "Per-bettor positions per market power the profile and market-detail pages.", + "Create the `bets` table (market_id FK, bettor, net_amount, gross_amount, is_yes, " + "claimed, created_at) with composite PK (market_id, bettor).", + ["Migration creates the table with the composite primary key", + "Foreign key to markets(id)", + "`idx_bets_bettor` index present"], + "db/migrations/0003_create_bets.sql") + +add("[DB] Create `leaderboard` table migration", + ["area:db", "P0"], MS_BACKEND, + "Leaderboard is served from a snapshot table rebuilt from events.", + "Create `leaderboard` (address PK, display_name, points, won_bets, lost_bets, " + "updated_at) plus `idx_lb_points(points DESC)`.", + ["Table + descending points index created", + "Re-runnable / forward-only as documented"], + "db/migrations/0004_create_leaderboard.sql") + +add("[DB] Create `events` audit-log table migration", + ["area:db", "P0"], MS_BACKEND, + "Raw on-chain events are stored for audit and replay.", + "Create `events` (id BIGSERIAL PK, ledger_seq, tx_hash, event_type, market_id, " + "actor, payload JSONB, created_at) + indexes on market_id, event_type, ledger_seq DESC.", + ["Table and three indexes created", + "payload is JSONB"], + "db/migrations/0005_create_events.sql") + +add("[DB] Build a SQL migration runner", + ["area:db", "P0"], MS_BACKEND, + "Migrations must apply in order and be tracked so we don't double-apply.", + "Implement a small migration runner (Node/TS) that applies `db/migrations/*.sql` " + "in filename order and records applied versions in a `schema_migrations` table.", + ["`npm run migrate` applies all pending migrations", + "Already-applied migrations are skipped", + "Failure rolls back the current migration in a transaction", + "`schema_migrations` table tracks applied filenames + timestamp"], + "db/migrate.ts (or backend/src/db/migrate.ts)") + +add("[DB] Postgres connection pool module", + ["area:db", "area:backend", "P0"], MS_BACKEND, + "Both backend and indexer need a shared, pooled pg client.", + "Create a reusable pg Pool wrapper reading `DATABASE_URL`, with sane pool size, " + "timeouts, and a `query()` helper that types rows.", + ["Single exported pool, configurable size via env", + "Typed `query` helper", + "Graceful shutdown closes the pool"], + "backend/src/db/pool.ts") + +add("[DB] Seed script for local development data", + ["area:db", "P1"], MS_BACKEND, + "Contributors need realistic local data without running a full indexer.", + "Write a seed script that inserts a handful of markets, bets, and leaderboard rows.", + ["`npm run seed` populates a local DB", + "Idempotent (safe to run twice)", + "Documented in db/README.md"], + "db/seed.ts") + +add("[DB] `oracle_submissions` table migration (Phase 2)", + ["area:db", "area:oracle", "P1"], MS_OPTIMISTIC, + "The optimistic/council oracle records each provider submission off-chain for monitoring.", + "Create `oracle_submissions` (id, market_id, submitter, outcome, bond_amount, " + "submitted_at, status) with indexes on market_id and status.", + ["Table + indexes created", + "status enum/check covers submitted|challenged|finalized|rejected"], + "db/migrations/0006_oracle_submissions.sql") + +add("[DB] `oracle_disputes` table migration (Phase 2)", + ["area:db", "area:oracle", "P1"], MS_OPTIMISTIC, + "Disputes/challenges need their own record for the dispute council flow.", + "Create `oracle_disputes` (id, submission_id FK, disputer, disputer_bond, " + "opened_at, resolved_at, resolution).", + ["Table created with FK to oracle_submissions", + "Index on submission_id"], + "db/migrations/0007_oracle_disputes.sql") + +for n, (title, what, loc) in enumerate([ + ("markets upsert query", "Implement `upsertMarket()` that inserts or updates a market row from decoded event data (ON CONFLICT id).", "backend/src/db/markets.ts"), + ("getMarkets paginated query", "Implement `getMarkets({filter, category, sort, page, limit})` returning rows + total count.", "backend/src/db/markets.ts"), + ("getMarketById query", "Implement `getMarketById(id)` returning a single market or null.", "backend/src/db/markets.ts"), + ("bets upsert query", "Implement `upsertBet()` accumulating net_amount on conflict (market_id, bettor).", "backend/src/db/bets.ts"), + ("getBetsByMarket query", "Implement paginated `getBetsByMarket(marketId, page, limit)`.", "backend/src/db/bets.ts"), + ("getBetsByBettor query", "Implement `getBetsByBettor(address)` for the profile page.", "backend/src/db/bets.ts"), + ("leaderboard upsert query", "Implement `upsertLeaderboardEntry()` adjusting points/won/lost.", "backend/src/db/leaderboard.ts"), + ("getLeaderboard query", "Implement paginated `getLeaderboard({offset, limit, sort})`.", "backend/src/db/leaderboard.ts"), + ("global stats query", "Implement `getGlobalStats()` (total markets, volume, users, bets).", "backend/src/db/stats.ts"), + ("event insert query", "Implement `insertEvent()` writing a raw event row (idempotent on tx_hash+log index).", "backend/src/db/events.ts"), + ("checkpoint store queries", "Implement `getCheckpoint()` / `saveCheckpoint(ledger)` for the indexer.", "backend/src/db/checkpoint.ts"), + ("DB transaction helper", "Add a `withTransaction(fn)` helper that begins/commits/rolls back.", "backend/src/db/tx.ts"), +], start=1): + add(f"[DB] Query: {title}", ["area:db", "P0" if n <= 8 else "P1"], MS_BACKEND, + "Typed query functions form the data-access layer shared by API and indexer.", + what, + ["Function is exported and typed", "Parameterized (no string interpolation of inputs)", + "Unit test covers the happy path", "Returns shapes documented in a TS type"], + loc) + +add("[DB] Define shared TS types for DB rows", + ["area:db", "P0"], MS_BACKEND, + "API and indexer must agree on row shapes.", + "Create TS interfaces for Market, Bet, LeaderboardEntry, EventRow mirroring the schema.", + ["Types exported from a single module", "Numeric/amount fields typed consistently", + "Reused by query functions"], + "backend/src/db/types.ts") + +add("[DB] Connection retry + health check", + ["area:db", "P1"], MS_BACKEND, + "Transient DB outages shouldn't crash services on boot.", + "Add startup retry with backoff and a `pingDb()` health function.", + ["Boot retries on ECONNREFUSED with capped backoff", "`pingDb()` returns ok/latency"], + "backend/src/db/health.ts") + +add("[DB] Document the schema in db/README.md", + ["area:db", "area:docs", "P2"], MS_BACKEND, + "Contributors need a single place describing every table and column.", + "Expand db/README.md with a table-by-table description and an ER diagram (mermaid).", + ["Every table documented", "Mermaid ER diagram renders on GitHub"], + "db/README.md") + + +# ────────────────────────────────────────────────────────────────────────────── +# INDEXER (area:indexer) — ~40 +# ────────────────────────────────────────────────────────────────────────────── +add("[INDEXER] getEvents RPC client with pagination", + ["area:indexer", "P0"], MS_BACKEND, + "The indexer reads contract events from Soroban RPC `getEvents()` page by page.", + "Implement a client that calls `getEvents({startLedger, filters, limit, cursor})` and " + "follows the cursor until the page is exhausted, returning decoded events + latestLedger.", + ["Handles cursor pagination within a poll", "Filters by the configured contract IDs", + "Returns latestLedger for checkpointing", "Surfaces RPC errors to the caller"], + "indexer/src/rpc/getEvents.ts") + +add("[INDEXER] Main polling loop with checkpointing", + ["area:indexer", "P0"], MS_BACKEND, + "The indexer must continuously poll, persist progress, and resume after restart.", + "Implement the loop: load checkpoint → indexEvents(from) → save checkpoint → sleep " + "POLL_INTERVAL_MS. On boot with no checkpoint, start from START_LEDGER.", + ["Resumes from the last saved checkpoint after restart", + "Configurable poll interval", "Loop survives a single iteration error (logs + continues)", + "Clean shutdown on SIGINT/SIGTERM saves checkpoint"], + "indexer/src/index.ts") + +add("[INDEXER] Topic/value decoder utilities (scValToNative)", + ["area:indexer", "P0"], MS_BACKEND, + "Raw XDR event topics and values must be decoded to JS values consistently.", + "Wrap `scValToNative` with helpers to decode an event's topics array and value into " + "a typed `{ type, subtype, data }`.", + ["Decodes topics and value", "Handles address, i128, bool, symbol, map types", + "Unit tests with sample XDR fixtures"], + "indexer/src/decode.ts") + +# Per-event-type handlers +for ev, desc in [ + ("market_created", "a new market is created — insert into markets"), + ("market_resolved", "a market is resolved — set resolved/outcome, invalidate caches"), + ("market_cancelled", "a market is cancelled — set cancelled=true"), + ("bet_placed", "a bet is placed — upsert bets, bump bet_count, update totals"), + ("claim", "a winner claims — mark bet claimed"), + ("reward_points", "points awarded — update leaderboard points"), + ("token_mint", "IPRED tokens minted to a user — optional balance tracking"), + ("referral_registered", "a referral relationship is registered"), + ("referral_reward", "a referrer earns a reward"), + ("fee_withdrawn", "admin withdraws accumulated fees"), +]: + add(f"[INDEXER] Handler: {ev} event", + ["area:indexer", "P0" if ev in ("market_created", "bet_placed", "market_resolved") else "P1"], + MS_BACKEND, + f"When {desc}.", + f"Implement the decoder + DB write for the `{ev}` event topic. Validate fields, " + "upsert the relevant table(s), and trigger cache invalidation where applicable.", + ["Decodes the event's specific payload", + "Writes/updates the correct table(s) idempotently", + "Invalidates affected Redis keys (if any)", + "Unit test with a sample event"], + f"indexer/src/handlers/{ev}.ts") + +add("[INDEXER] Handler dispatch registry", + ["area:indexer", "P0"], MS_BACKEND, + "A single place maps an event type to its handler.", + "Implement a registry/dispatch that routes a decoded event to the right handler by topic.", + ["Unknown event types are logged and skipped (not fatal)", + "Adding a handler is one registry line", "Dispatch is unit-tested"], + "indexer/src/handlers/index.ts") + +add("[INDEXER] Idempotent event writes (dedupe on tx_hash + index)", + ["area:indexer", "P0"], MS_BACKEND, + "Re-polling overlapping ledgers must not double-count bets or duplicate rows.", + "Ensure every write path is idempotent: dedupe events by (tx_hash, event_index) and " + "use ON CONFLICT upserts so re-processing is safe.", + ["Re-running the indexer over the same ledger range produces identical DB state", + "Integration test replays a batch twice with no drift"], + "indexer/src/handlers/") + +add("[INDEXER] Historical backfill mode", + ["area:indexer", "P1"], MS_BACKEND, + "On first deploy we must index all history from the contract's first ledger.", + "Add a backfill mode that pages from START_LEDGER to head as fast as RPC allows, " + "then hands off to the live loop.", + ["`--backfill` indexes from START_LEDGER to current head", + "Respects RPC rate limits / retries on 429", + "Transitions to live polling when caught up"], + "indexer/src/backfill.ts") + +add("[INDEXER] Ledger reorg / gap detection", + ["area:indexer", "P1"], MS_BACKEND, + "If the indexer falls behind RPC's event retention window we must detect the gap.", + "Detect when startLedger is older than RPC's oldest retained ledger and surface a " + "clear error + recovery path (re-backfill from snapshot).", + ["Detects 'startLedger too old' RPC errors", + "Logs a clear remediation message", "Does not silently skip ledgers"], + "indexer/src/rpc/getEvents.ts") + +add("[INDEXER] RPC retry with exponential backoff", + ["area:indexer", "P0"], MS_BACKEND, + "Public RPC nodes rate-limit and occasionally fail; the indexer must be resilient.", + "Wrap RPC calls with retry + exponential backoff + jitter, capped, with 429 awareness.", + ["Retries transient failures", "Backs off on 429", "Gives up after a cap and surfaces the error"], + "indexer/src/rpc/retry.ts") + +add("[INDEXER] Config loading + validation", + ["area:indexer", "P0"], MS_BACKEND, + "Misconfiguration should fail fast at boot, not mid-loop.", + "Load and validate env (DATABASE_URL, SOROBAN_RPC_URL, contract IDs, intervals) with Zod.", + ["Missing/invalid env fails boot with a clear message", "Typed config object exported"], + "indexer/src/config/index.ts") + +add("[INDEXER] Structured logging", + ["area:indexer", "P1"], MS_BACKEND, + "We need queryable logs for indexer lag and errors.", + "Add structured (JSON) logging with levels and per-iteration summary (events processed, lag).", + ["JSON logs with level + timestamp", "Per-poll summary line", "Configurable log level"], + "indexer/src/log.ts") + +add("[INDEXER] Expose indexer lag metric hook", + ["area:indexer", "area:monitoring", "P1"], MS_MONITOR, + "Monitoring needs to know how far behind head the indexer is.", + "Compute `latestLedger - checkpoint` each poll and expose it for the metrics endpoint.", + ["Lag computed each poll", "Exported for Prometheus collector"], + "indexer/src/metrics.ts") + +add("[INDEXER] Graceful shutdown & checkpoint flush", + ["area:indexer", "P1"], MS_BACKEND, + "On deploy/restart we must not lose or repeat work.", + "Trap SIGINT/SIGTERM, finish the current event, flush checkpoint, close DB/Redis, exit 0.", + ["No partial event left half-written", "Checkpoint saved before exit", "Connections closed"], + "indexer/src/index.ts") + +for n, (title, what) in enumerate([ + ("totals recomputation from bets", "Add a job to recompute market total_yes/total_no from bets as a consistency check."), + ("bet_count backfill", "Add a maintenance task to recompute bet_count per market from the bets table."), + ("leaderboard rebuild job", "Add a job that rebuilds the leaderboard table from the events log."), + ("event payload schema validation", "Validate decoded event payloads against Zod schemas before writing."), + ("dead-letter for undecodable events", "Persist events that fail to decode to a dead-letter table for inspection."), + ("indexer dockerfile", "Write a multi-stage Dockerfile for the indexer."), + ("indexer integration test harness", "Set up a test harness with a throwaway Postgres (testcontainers or compose)."), + ("contract-id allowlist filter", "Make the indexed contract set configurable and validated."), + ("poll-loop unit tests", "Unit-test the loop logic with a mocked RPC + DB."), + ("metrics: events_processed counter", "Increment an events_processed counter per handled event."), +], start=1): + ms = MS_TESTING if "test" in title else (MS_INFRA if "docker" in title else (MS_MONITOR if "metric" in title else MS_BACKEND)) + labels = ["area:indexer"] + if "test" in title: labels.append("area:testing") + if "docker" in title: labels.append("area:infra") + if "metric" in title: labels.append("area:monitoring") + labels.append("P1") + add(f"[INDEXER] {title}", labels, ms, + "Hardening and operational completeness for the indexer.", + what, + ["Implemented per description", "Covered by a test or documented runbook step"], + "indexer/src/") + + +# ────────────────────────────────────────────────────────────────────────────── +# BACKEND API (area:backend / area:api) — ~55 +# ────────────────────────────────────────────────────────────────────────────── +add("[API] Bootstrap Fastify server", + ["area:backend", "P0"], MS_BACKEND, + "Everything else hangs off a configured HTTP server.", + "Replace the scaffold entry with a Fastify instance: logging, JSON, graceful shutdown, " + "listen on PORT/HOST, health route `/healthz`.", + ["Server starts and responds 200 on `/healthz`", + "Reads PORT/HOST from config", "Graceful shutdown on SIGTERM"], + "backend/src/server.ts") + +add("[API] Config loading + validation (Zod)", + ["area:backend", "P0"], MS_BACKEND, + "Fail fast on misconfiguration.", + "Load + validate all backend env vars with Zod; export a typed config.", + ["Invalid env fails boot with a readable error", "Typed config exported", "No process.env reads outside config"], + "backend/src/config/index.ts") + +add("[API] Central error handler + error shape", + ["area:backend", "P0"], MS_BACKEND, + "Clients need consistent error responses; we must not leak stack traces.", + "Implement a Fastify error handler returning `{ error: { code, message } }` with proper " + "status codes; map known errors, hide internals on 500.", + ["Consistent error JSON shape", "4xx vs 5xx mapped correctly", "No stack traces in responses"], + "backend/src/lib/errors.ts") + +add("[API] Request validation middleware (Zod schemas)", + ["area:backend", "area:api", "P0"], MS_BACKEND, + "Query/body params must be validated before hitting the DB.", + "Provide a helper to validate request query/params/body against Zod and return 400 on failure.", + ["Invalid query returns 400 with field errors", "Reusable across routes", "Typed handler input after validation"], + "backend/src/lib/validate.ts") + +# Markets endpoints +add("[API] GET /api/markets — list with filter/category/sort/pagination", + ["area:api", "area:backend", "P0"], MS_BACKEND, + "The primary endpoint the markets page calls.", + "Implement GET /api/markets supporting filter=active|resolved|ended|cancelled, " + "category, sort=newest|volume|ending_soon|bettors, page, limit. Return " + "`{ markets, total, page }`.", + ["All query params validated + defaulted", "Pagination correct (total reflects filter)", + "Sort options work", "Reads from DB layer, not RPC", "Integration test covers filters"], + "backend/src/api/markets.ts") + +add("[API] GET /api/markets/:id — single market", + ["area:api", "area:backend", "P0"], MS_BACKEND, + "Market detail page needs one market by id.", + "Implement GET /api/markets/:id returning the market or 404.", + ["Returns the market", "404 for unknown id", "id validated as a positive integer"], + "backend/src/api/markets.ts") + +add("[API] GET /api/markets/:id/bets — paginated bets for a market", + ["area:api", "area:backend", "P0"], MS_BACKEND, + "Market detail shows recent bets.", + "Implement GET /api/markets/:id/bets?page&limit returning `{ bets, total }`.", + ["Pagination works", "404 if market missing", "Bets sorted newest-first"], + "backend/src/api/markets.ts") + +add("[API] GET /api/leaderboard — paginated leaderboard", + ["area:api", "area:backend", "P0"], MS_BACKEND, + "Leaderboard page + homepage preview.", + "Implement GET /api/leaderboard?offset&limit&sort=points|bets returning `{ players, total }`.", + ["Offset/limit validated", "Sort by points and by bets", "Reads from leaderboard table"], + "backend/src/api/leaderboard.ts") + +add("[API] GET /api/stats — global stats", + ["area:api", "area:backend", "P0"], MS_BACKEND, + "Homepage LiveStats needs aggregate numbers.", + "Implement GET /api/stats returning totalMarkets, totalVolume, totalUsers, totalBets.", + ["Returns all four metrics", "Backed by an efficient aggregate query", "Cacheable"], + "backend/src/api/stats.ts") + +add("[API] GET /api/profile/:address — user profile aggregate", + ["area:api", "area:backend", "P1"], MS_BACKEND, + "Profile page needs a user's bets, points, win/loss in one call.", + "Implement GET /api/profile/:address aggregating bets + leaderboard entry for an address.", + ["Validates Stellar address format (G..., 56 chars)", "Returns bets + points + win/loss", + "404 or empty profile for unknown address (documented choice)"], + "backend/src/api/profile.ts") + +add("[API] POST /api/oracle/submit — provider submission intake", + ["area:api", "area:oracle", "P1"], MS_OPTIMISTIC, + "Oracle providers submit outcomes through the backend (auth + record + forward).", + "Implement POST /api/oracle/submit { marketId, outcome, signature, provider } guarded by " + "API-key auth; record the submission and return `{ accepted, submissionsNeeded }`.", + ["Bearer/API-key auth enforced", "Body validated", "Submission recorded in oracle_submissions", + "Returns remaining submissions to threshold"], + "backend/src/api/oracle.ts") + +# Cross-cutting API issues +for title, what, loc, ms, extra_labels in [ + ("Route registration + versioning (/api/v1)", "Register all routes under /api/v1 and add a route index.", "backend/src/api/index.ts", MS_BACKEND, []), + ("CORS configuration", "Configure CORS to allow the frontend origin(s) only.", "backend/src/server.ts", MS_BACKEND, []), + ("Security headers (helmet)", "Add standard security headers.", "backend/src/server.ts", MS_BACKEND, []), + ("Request logging + request-id", "Log each request with a correlation id.", "backend/src/lib/log.ts", MS_BACKEND, []), + ("OpenAPI / Swagger spec", "Generate and serve an OpenAPI spec at /api/docs.", "backend/src/api/openapi.ts", MS_BACKEND, ["area:docs"]), + ("Standard pagination helper", "Shared offset/limit parsing + total-count envelope.", "backend/src/lib/pagination.ts", MS_BACKEND, []), + ("Health: /readyz checks DB+Redis", "Readiness probe verifying DB and Redis connectivity.", "backend/src/api/health.ts", MS_INFRA, ["area:infra"]), + ("Graceful shutdown drains in-flight requests", "Stop accepting, drain, then close.", "backend/src/server.ts", MS_BACKEND, []), + ("404 + method-not-allowed handlers", "Consistent handling for unknown routes/methods.", "backend/src/lib/errors.ts", MS_BACKEND, []), + ("Response compression", "gzip/br compression for list endpoints.", "backend/src/server.ts", MS_BACKEND, []), + ("ETag / conditional GET for market lists", "Support ETag to cut payload on unchanged lists.", "backend/src/api/markets.ts", MS_CACHE, ["area:cache"]), + ("API integration test setup", "Test harness booting the app against a test DB.", "backend/test/setup.ts", MS_TESTING, ["area:testing"]), + ("Markets endpoint integration tests", "Cover filter/sort/pagination + 404.", "backend/test/markets.test.ts", MS_TESTING, ["area:testing"]), + ("Leaderboard endpoint integration tests", "Cover sort + pagination.", "backend/test/leaderboard.test.ts", MS_TESTING, ["area:testing"]), + ("Stats endpoint test", "Verify aggregate values against seeded data.", "backend/test/stats.test.ts", MS_TESTING, ["area:testing"]), + ("Input fuzz/edge tests for pagination", "Negative/huge/non-numeric page & limit.", "backend/test/pagination.test.ts", MS_TESTING, ["area:testing"]), + ("Backend Dockerfile", "Multi-stage Dockerfile for the API.", "backend/Dockerfile", MS_INFRA, ["area:infra"]), + ("API error-rate metric hook", "Count 5xx responses for monitoring.", "backend/src/metrics.ts", MS_MONITOR, ["area:monitoring"]), + ("Request-duration histogram", "Record per-route latency.", "backend/src/metrics.ts", MS_MONITOR, ["area:monitoring"]), + ("Sanitize/normalize category param", "Map/validate category against the known set.", "backend/src/api/markets.ts", MS_BACKEND, []), + ("Sort=volume backed by total_yes+total_no", "Implement volume sort correctly.", "backend/src/db/markets.ts", MS_BACKEND, ["area:db"]), + ("Sort=ending_soon excludes resolved", "Ending-soon must ignore resolved/cancelled.", "backend/src/db/markets.ts", MS_BACKEND, ["area:db"]), + ("Empty-state responses are well-formed", "Empty lists return {items:[],total:0} not null.", "backend/src/lib/pagination.ts", MS_BACKEND, []), + ("Address normalization util", "Uppercase/validate G-addresses centrally.", "backend/src/lib/address.ts", MS_BACKEND, []), + ("Amount formatting (stroops↔XLM) util", "Centralize 7-decimal conversions.", "backend/src/lib/amount.ts", MS_BACKEND, []), + ("Config: per-route rate-limit table", "Encode the design-doc rate-limit table.", "backend/src/config/rateLimits.ts", MS_CACHE, ["area:cache"]), + ("Frontend client: point app at backend API", "Add a feature-flagged data source in the frontend that reads the backend.", "frontend/src/services/api.ts", MS_BACKEND, []), + ("Frontend: env flag NEXT_PUBLIC_USE_BACKEND", "Toggle between direct-RPC and backend reads.", "frontend/src/config/network.ts", MS_BACKEND, []), + ("Contract test: API response shapes match frontend types", "Ensure API shapes match frontend Market/Bet types.", "backend/test/contract.test.ts", MS_TESTING, ["area:testing"]), + ("Document all endpoints in backend/README", "Keep README endpoint list authoritative.", "backend/README.md", MS_BACKEND, ["area:docs"]), +]: + labels = ["area:backend"] + extra_labels + ["P1"] + add(f"[API] {title}", labels, ms, + "Cross-cutting backend concern for a production-grade API.", + what, + ["Implemented per description", "Has a test or is exercised by an existing test", "No regression to existing endpoints"], + loc) + + +# ────────────────────────────────────────────────────────────────────────────── +# CACHE (area:cache) — ~15 +# ────────────────────────────────────────────────────────────────────────────── +add("[CACHE] Redis client module", + ["area:cache", "P0"], MS_CACHE, + "All caching goes through one configured Redis client.", + "Create an ioredis client reading REDIS_URL with reconnect strategy and a typed get/set/del helper.", + ["Single exported client", "Reconnect on drop", "Typed JSON get/set with TTL", "Graceful close"], + "backend/src/cache/redis.ts") + +add("[CACHE] Cache-aside helper (getOrSet)", + ["area:cache", "P0"], MS_CACHE, + "Endpoints should transparently cache reads.", + "Implement `getOrSet(key, ttl, loader)` that returns cached value or computes+stores it.", + ["Returns cached on hit", "Calls loader + stores on miss", "Stampede-safe (single-flight) or documented", + "Unit tested with a fake redis"], + "backend/src/cache/cacheAside.ts") + +add("[CACHE] Cache keys + TTL table from design doc", + ["area:cache", "P0"], MS_CACHE, + "Keys/TTLs must match the design doc.", + "Encode markets:all(30s), markets:active(15s), market:{id}(30s), leaderboard:top20(60s), " + "stats:global(60s), bets:{id}(30s) as a typed key/TTL registry.", + ["Centralized key builders", "TTLs match the doc", "No stringly-typed keys at call sites"], + "backend/src/cache/keys.ts") + +add("[CACHE] Wire markets endpoints through cache", + ["area:cache", "area:api", "P1"], MS_CACHE, + "Markets list/detail are the hottest reads.", + "Use getOrSet for GET /api/markets and /api/markets/:id with the right keys/TTLs.", + ["Cache hit avoids DB", "Correct keys per filter/category/page", "Hit/miss observable in logs/metrics"], + "backend/src/api/markets.ts") + +add("[CACHE] Wire leaderboard + stats through cache", + ["area:cache", "area:api", "P1"], MS_CACHE, + "Leaderboard and stats change slowly; cache them.", + "Apply getOrSet to leaderboard and stats endpoints.", + ["Both endpoints cached with doc TTLs", "Stale data bounded by TTL"], + "backend/src/api/leaderboard.ts") + +add("[CACHE] Invalidation API + event hooks", + ["area:cache", "area:indexer", "P1"], MS_CACHE, + "When the indexer writes updates, stale caches must be cleared.", + "Expose invalidate(keys) and call it from indexer handlers: market created → markets:all/active; " + "bet → market:{id}, markets:active; resolved → all market caches + leaderboard.", + ["Invalidation helper exported", "Each relevant handler invalidates the right keys", + "Integration test: write → cache cleared"], + "backend/src/cache/invalidate.ts") + +for title, what in [ + ("Rate limiter (Redis sliding window)", "Implement a per-IP sliding-window limiter in Redis."), + ("Apply per-route rate limits", "Hook the limiter into routes using the rate-limit table."), + ("Rate-limit headers (X-RateLimit-*)", "Return remaining/limit/reset headers."), + ("Cache hit-rate metric", "Track and expose cache hit rate."), + ("Negative caching for 404 markets", "Briefly cache misses to avoid DB hammering."), + ("Cache key versioning / namespace", "Prefix keys with a version to allow bulk bust."), + ("Graceful degradation when Redis down", "Fall back to DB when Redis is unavailable, log it."), + ("Cache unit tests", "Test getOrSet, invalidation, and limiter logic."), + ("Bets list caching", "Cache GET /api/markets/:id/bets with 30s TTL + invalidation."), +]: + labels = ["area:cache"] + if "metric" in title: labels.append("area:monitoring") + if "test" in title: labels.append("area:testing") + labels.append("P1") + add(f"[CACHE] {title}", labels, MS_CACHE, + "Caching/rate-limiting layer for scale.", + what, + ["Implemented per description", "Tested or exercised", "Documented in code"], + "backend/src/cache/") + + +# ────────────────────────────────────────────────────────────────────────────── +# ORACLE — COUNCIL (Phase 1.5) — ~25 +# ────────────────────────────────────────────────────────────────────────────── +add("[ORACLE] Council aggregator service skeleton", + ["area:oracle", "P0"], MS_COUNCIL, + "Phase 1.5 uses a 4-of-7 council; an off-chain aggregator watches their submissions and " + "fires the final on-chain resolution once threshold is met. Zero contract changes.", + "Build the aggregator skeleton: load config (council members, threshold), connect to RPC/DB, " + "run a loop over expired-unresolved markets.", + ["Loads COUNCIL_SIZE/COUNCIL_THRESHOLD from config", "Lists expired unresolved markets", + "Loop scaffold with clean shutdown"], + "oracle/src/aggregator/index.ts") + +add("[ORACLE] Track council member submissions per market", + ["area:oracle", "P0"], MS_COUNCIL, + "We must count how many council members have submitted which outcome.", + "Persist each council member's submitted outcome per market and compute the current tally.", + ["Per-market tally of yes/no by member", "No double-counting a member", + "Tally readable by the finalizer"], + "oracle/src/aggregator/tally.ts") + +add("[ORACLE] Detect threshold reached (4-of-7) and select outcome", + ["area:oracle", "P0"], MS_COUNCIL, + "Finalization triggers only when ≥ threshold members agree.", + "Implement logic that returns the agreed outcome when any side reaches COUNCIL_THRESHOLD, " + "else null.", + ["Returns outcome when threshold met", "Returns null below threshold", + "Handles split votes (no premature finalize)", "Unit tested"], + "oracle/src/aggregator/threshold.ts") + +add("[ORACLE] Submit final resolve_market tx (council)", + ["area:oracle", "area:contracts", "P0"], MS_COUNCIL, + "Once threshold is met, the aggregator submits the on-chain resolution using a resolver key.", + "Build, sign, and submit `resolve_market(caller, market_id, outcome)` from a configured " + "resolver account; poll for confirmation; record result.", + ["Builds + signs + submits the tx", "Idempotent (won't resolve an already-resolved market)", + "Records tx hash + outcome", "Handles failure with retry/alert"], + "oracle/src/submitter/resolveMarket.ts") + +add("[ORACLE] Council member registry + config", + ["area:oracle", "P0"], MS_COUNCIL, + "We need the 7 council addresses and the resolver key wired safely.", + "Load council member public keys and the resolver signing key from env/secret store; validate.", + ["7 member keys validated", "Resolver secret loaded from env (never logged)", + "Boot fails if misconfigured"], + "oracle/src/config/council.ts") + +for title, what in [ + ("Council submission CLI for members", "A CLI a council member runs to record their outcome for a market."), + ("Conflicting-submission detection", "Flag markets where members disagree past a threshold."), + ("Stuck-market detection (>6h past expiry)", "Alert when an expired market has no quorum after N hours."), + ("Aggregator dry-run mode", "Run the full flow without submitting on-chain (for testing)."), + ("Resolver key rotation support", "Allow swapping the resolver key without downtime."), + ("Aggregator persistence of decisions", "Persist finalize decisions + tx hashes to oracle_submissions."), + ("Replay protection for finalize", "Never submit two resolutions for one market."), + ("Aggregator metrics (resolution lag)", "Expose hours-from-expiry-to-resolution."), + ("Council flow integration test", "End-to-end test against a local/testnet contract."), + ("Council runbook docs", "Document how council members submit and how aggregator finalizes."), + ("Handle market cancellation path", "Aggregator must skip/!finalize cancelled markets."), + ("Quorum config validation", "Threshold must be > half of size; validate at boot."), + ("Backoff on RPC failure during submit", "Retry submit with backoff; alert on persistent failure."), + ("Aggregator structured logging", "JSON logs of tallies and decisions."), + ("Aggregator Dockerfile", "Containerize the aggregator service."), + ("Aggregator config: poll interval", "Make the scan interval configurable."), + ("Per-category resolver mapping (optional)", "Allow different resolver sets per category."), + ("Safety: refuse finalize if data missing", "Don't finalize without recorded member submissions."), + ("Notify on successful finalize", "Send a webhook/log when a market is finalized."), + ("Audit export of council decisions", "Export decisions+tallies to CSV/JSON for audit."), +]: + labels = ["area:oracle"] + if "metric" in title: labels.append("area:monitoring") + if "test" in title: labels.append("area:testing") + if "docs" in title or "runbook" in title: labels.append("area:docs") + if "Dockerfile" in title: labels.append("area:infra") + labels.append("P1") + add(f"[ORACLE] {title}", labels, MS_COUNCIL, + "Council (Phase 1.5) resolution operational completeness.", + what, + ["Implemented per description", "Tested or covered by a runbook", "No path can finalize a market twice"], + "oracle/src/aggregator/") + + +# ────────────────────────────────────────────────────────────────────────────── +# ORACLE — OPTIMISTIC (Phase 2) — ~30 (contract + off-chain) +# ────────────────────────────────────────────────────────────────────────────── +add("[CONTRACTS] Optimistic oracle: new DataKey variants + state", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "Phase 2 adds bonded optimistic resolution to the prediction_market contract.", + "Add storage for submissions (outcome, submitter, bond, timestamp), challenge state, and " + "the OPEN→SUBMITTED→CHALLENGED→ESCALATED→FINALIZED state machine fields.", + ["New DataKey variants compile", "State enum models the doc's lifecycle", + "Existing tests still pass", "No storage collision with current keys"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] submit_outcome(market_id, outcome, bond)", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "Anyone can post an outcome with a bond to start the optimistic window.", + "Implement `submit_outcome` that escrows SUBMITTER_BOND, records the submission, and starts " + "the challenge window. Reject if already submitted/resolved.", + ["Escrows the submitter bond", "Sets state SUBMITTED + window start", + "Rejects double submission", "Emits an event", "Unit tested"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] challenge(market_id, bond)", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "A disputer can challenge within the window by posting a larger bond.", + "Implement `challenge` requiring DISPUTER_BOND > submitter bond, within the window; moves " + "state to CHALLENGED → ESCALATED.", + ["Requires larger bond", "Only within challenge window", "Moves to escalated state", + "Emits event", "Unit tested"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] finalize_unchallenged(market_id)", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "If unchallenged after the window, anyone can finalize the submitted outcome.", + "Implement finalize that, after the window with no challenge, sets the market resolved to the " + "submitted outcome and returns the submitter's bond.", + ["Only after window elapsed", "Sets resolved+outcome", "Returns submitter bond", + "Callable by anyone", "Unit tested"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] council_resolve_dispute(market_id, outcome)", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "Escalated disputes are settled by the dispute council vote.", + "Implement council resolution that distributes bonds per the doc (winner gets bonds, 10% council " + "fee from loser) and finalizes the market.", + ["Council-gated", "Bond distribution matches the doc", "Council fee taken", + "Finalizes market", "Unit tested"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] Bond constants + accounting", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "Bonds must be tracked and refunded/distributed exactly.", + "Add SUBMITTER_BOND/DISPUTER_BOND constants and bond ledger entries; ensure no XLM is lost or " + "double-spent across all paths.", + ["Constants match the doc", "Every bond has a payout path", "Sum-in == sum-out invariant tested"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] Events for oracle lifecycle", + ["area:contracts", "area:oracle", "P1"], MS_OPTIMISTIC, + "The indexer/monitor watch submission/challenge/finalize events.", + "Emit events for submission, challenge, escalation, finalization with relevant fields.", + ["Each transition emits a typed event", "Fields match the indexer handlers", "Documented topics"], + "contracts/prediction_market/src/") + +add("[CONTRACTS] Tests for optimistic state machine", + ["area:contracts", "area:testing", "P1"], MS_OPTIMISTIC, + "The bonded flow is security-critical and needs thorough tests.", + "Cover happy path (unchallenged finalize), challenged→council, invalid transitions, and bond math.", + ["Unchallenged finalize tested", "Challenge→council tested", "Invalid transitions rejected", + "Bond conservation asserted"], + "contracts/prediction_market/src/test.rs") + +for title, what in [ + ("Indexer handler: oracle submission event", "Index submit_outcome events into oracle_submissions."), + ("Indexer handler: challenge event", "Index challenge events into oracle_disputes."), + ("Indexer handler: finalize event", "Index finalize/resolution events and update markets."), + ("Monitor: submission watcher", "Watch new submissions and surface them."), + ("Monitor: challenge watcher", "Alert when a submission is challenged."), + ("Monitor: dispute escalation watcher", "Alert when a dispute escalates to council."), + ("Monitor: bond-below-minimum alert", "Alert if a bond is below the required minimum."), + ("Monitor: council inactivity alert (48h)", "Alert when council hasn't acted in 48h."), + ("Off-chain submitter for optimistic outcomes", "Service that posts outcomes+bonds from data adapters."), + ("Challenge bot (defensive)", "Optional bot that challenges clearly-wrong submissions."), + ("Bond balance/risk dashboard data", "Expose total bonded XLM and at-risk markets."), + ("Optimistic flow integration test (off-chain)", "Drive submit→finalize via the off-chain services on testnet."), + ("Dispute council membership config", "Configure and validate the dispute council set."), + ("Optimistic oracle runbook", "Document submit/challenge/finalize operations."), + ("Replay protection for off-chain submitter", "Never submit twice for the same market."), + ("Timeout handling for escalated disputes", "Handle disputes that exceed the council window."), + ("Bond refund reconciliation job", "Reconcile expected vs actual bond refunds."), + ("Outcome confidence gating", "Only auto-submit when data confidence is high."), + ("Optimistic config: windows + bonds", "Make challenge/dispute windows + bonds configurable."), + ("Security review checklist for oracle", "A checklist PR template for oracle changes."), + ("Metrics: oracle_submissions/disputes counters", "Expose submission/dispute counters."), + ("Metrics: oracle_resolution_lag_h", "Expose hours from expiry to finalization."), +]: + labels = ["area:oracle"] + if title.startswith("Indexer"): labels.append("area:indexer") + if "Monitor" in title or "alert" in title: labels.append("area:monitoring") + if "Metrics" in title: labels.append("area:monitoring") + if "test" in title: labels.append("area:testing") + if "runbook" in title or "review checklist" in title: labels.append("area:docs") + labels.append("P1") + add(f"[ORACLE] {title}", labels, MS_OPTIMISTIC, + "Phase 2 optimistic oracle off-chain + monitoring completeness.", + what, + ["Implemented per description", "Tested or covered by a runbook", "No double-submit / double-payout path"], + "oracle/src/") + + +# ────────────────────────────────────────────────────────────────────────────── +# ORACLE — DATA ADAPTERS — ~25 +# ────────────────────────────────────────────────────────────────────────────── +add("[ORACLE] Data adapter interface + registry", + ["area:oracle", "P1"], MS_DATA, + "Each market category resolves from one or more data sources; we need a common interface.", + "Define `DataAdapter` { id, supports(market), fetchOutcome(market): {outcome, confidence, raw} } " + "and a registry that picks adapters per category.", + ["Interface defined + documented", "Registry selects adapters by category", + "Adapters are independently testable"], + "oracle/src/adapters/index.ts") + +add("[ORACLE] Outcome normalization + confidence model", + ["area:oracle", "P1"], MS_DATA, + "Different sources return different shapes; we normalize to a yes/no + confidence.", + "Implement normalization that turns raw source data into a boolean outcome + confidence score, " + "with a documented mapping per category.", + ["Normalizes to {outcome, confidence}", "Confidence in [0,1]", "Unit tests per mapping"], + "oracle/src/adapters/normalize.ts") + +add("[ORACLE] Multi-source agreement / fallback", + ["area:oracle", "P1"], MS_DATA, + "Primary/secondary/tertiary sources must agree or fall back per the doc's source table.", + "Implement logic that queries primary→secondary→tertiary and decides outcome when sources agree, " + "else flags for manual review.", + ["Falls back on primary failure", "Requires agreement or flags conflict", + "Configurable per category"], + "oracle/src/adapters/resolve.ts") + +for src, cat, detail in [ + ("CoinGecko", "Crypto", "price/market-cap queries"), + ("Binance", "Crypto", "ticker/price queries"), + ("CoinMarketCap", "Crypto", "price fallback"), + ("TheOddsAPI", "Sports", "game results/odds"), + ("SportDataAPI", "Sports", "scores/results"), + ("ESPN feed", "Sports", "results fallback"), + ("Metaculus", "Politics", "forecast/resolution"), + ("Polymarket feed", "Politics", "market resolution reference"), + ("Reuters", "Politics", "news-based confirmation"), +]: + add(f"[ORACLE] Adapter: {src} ({cat})", + ["area:oracle", "P1"], MS_DATA, + f"{cat} markets resolve from {src} ({detail}).", + f"Implement a `{src}` adapter: client, rate-limit handling, map a market to a query, return " + "a normalized outcome + confidence + raw payload.", + ["Implements the DataAdapter interface", "Handles API errors/rate limits + retries", + "Maps market → query correctly", "Unit test with a recorded fixture response"], + f"oracle/src/adapters/{src.lower().replace(' ', '')}.ts") + +for title, what in [ + ("API key management for adapters", "Load/validate all data-source API keys from env."), + ("Adapter response caching", "Cache source responses briefly to cut quota usage."), + ("Adapter rate-limit coordination", "Shared limiter across adapters per provider."), + ("Market→query mapping schema", "Structured mapping stored per market (e.g. asset, threshold, date)."), + ("Manual-review queue for low confidence", "Route low-confidence/conflicting outcomes to a review queue."), + ("Adapter health checks", "Ping each source and report availability."), + ("Adapter fixtures + replay tests", "Record real responses and test deterministically."), + ("Sports: handle postponed/cancelled games", "Map postponed games to market cancellation."), + ("Crypto: timezone/snapshot-time correctness", "Resolve price at the exact market deadline (UTC)."), + ("Politics: conservative confidence gating", "Require higher agreement for political outcomes."), + ("Adapter selection by market metadata", "Pick adapters from a market's category + tags."), + ("Adapter error taxonomy + logging", "Classify source errors (auth/quota/network/data)."), + ("Adapters runbook + source matrix docs", "Document each source, its limits, and mapping."), +]: + labels = ["area:oracle"] + if "test" in title or "fixtures" in title: labels.append("area:testing") + if "docs" in title or "runbook" in title: labels.append("area:docs") + if "health" in title: labels.append("area:monitoring") + labels.append("P1") + add(f"[ORACLE] {title}", labels, MS_DATA, + "Data-adapter layer that feeds outcomes to the oracle.", + what, + ["Implemented per description", "Tested with fixtures where applicable", "Quota-safe"], + "oracle/src/adapters/") + + +# ────────────────────────────────────────────────────────────────────────────── +# INFRA (area:infra) — ~15 +# ────────────────────────────────────────────────────────────────────────────── +for title, what, loc in [ + ("Production docker-compose (api/indexer/pg/redis)", "Write docker-compose.production.yml with replicated api, single indexer, postgres, redis per the doc.", "infra/docker-compose.production.yml"), + ("Oracle services in compose", "Add aggregator/monitor services to production compose.", "infra/docker-compose.production.yml"), + ("Centralized .env loading + secrets doc", "Document required env across all services + secret handling.", "infra/README.md"), + ("Postgres backup/restore scripts", "Scripts for pg_dump backup + restore.", "infra/scripts/backup.sh"), + ("Redis persistence config", "Configure AOF/RDB persistence appropriately.", "infra/redis.conf"), + ("Healthcheck wiring for all services", "Add container healthchecks + depends_on conditions.", "infra/docker-compose.production.yml"), + ("Reverse proxy / TLS termination", "Add nginx/caddy in front of the API with TLS.", "infra/proxy/"), + ("Resource limits + restart policies", "Set memory/cpu limits and restart=always.", "infra/docker-compose.production.yml"), + ("Migration step in deploy flow", "Run DB migrations before starting api/indexer.", "infra/scripts/deploy.sh"), + ("Log aggregation config", "Ship container logs to a central sink.", "infra/logging/"), + ("Staging environment compose", "A staging compose pointing at testnet.", "infra/docker-compose.staging.yml"), + ("Secrets via env-file / vault placeholder", "Wire a secrets source (env-file now, vault later).", "infra/README.md"), + ("Indexer single-instance lock", "Ensure only one indexer runs (advisory lock).", "indexer/src/lock.ts"), + ("Container image tagging convention", "Document/set image tag scheme.", "infra/README.md"), + ("Local make/justfile for common tasks", "Add a task runner for up/migrate/seed/test.", "Justfile"), +]: + labels = ["area:infra", "P1"] + if "migration" in title.lower() or "indexer" in title.lower(): labels.insert(1, "area:db" if "migration" in title.lower() else "area:indexer") + add(f"[INFRA] {title}", labels, MS_INFRA, + "Infrastructure for running the backend stack reliably.", + what, + ["Implemented per description", "Brought up cleanly with `docker compose up`", "Documented in infra/README.md"], + loc) + + +# ────────────────────────────────────────────────────────────────────────────── +# MONITORING (area:monitoring) — ~15 +# ────────────────────────────────────────────────────────────────────────────── +for title, what, loc in [ + ("Prometheus metrics endpoint (backend)", "Expose /metrics on the API with default + custom metrics.", "backend/src/metrics.ts"), + ("Prometheus metrics endpoint (indexer)", "Expose indexer metrics (lag, events, rpc errors).", "indexer/src/metrics.ts"), + ("Prometheus metrics endpoint (oracle)", "Expose oracle metrics (submissions, disputes, lag).", "oracle/src/metrics.ts"), + ("Metric: indexer_lag_ledgers gauge", "Implement the indexer lag gauge.", "indexer/src/metrics.ts"), + ("Metric: rpc_errors_total counter", "Count RPC failures across services.", "indexer/src/metrics.ts"), + ("Metric: cache_hit_rate gauge", "Compute and expose cache hit rate.", "backend/src/metrics.ts"), + ("Metric: api_request_duration histogram", "Per-route latency histogram.", "backend/src/metrics.ts"), + ("Business metrics counters", "markets_created/bets_placed/volume/resolved counters.", "backend/src/metrics.ts"), + ("Grafana dashboard: system health", "Dashboard JSON for indexer/api/db/redis health.", "infra/grafana/system.json"), + ("Grafana dashboard: business metrics", "Dashboard for markets/bets/volume.", "infra/grafana/business.json"), + ("Grafana dashboard: oracle health", "Dashboard for submissions/disputes/lag.", "infra/grafana/oracle.json"), + ("Alert rules (Prometheus)", "Encode IndexerStalled/HighRPCErrorRate/MarketStuck/HighAPILatency/DatabaseSlow.", "infra/prometheus/alerts.yml"), + ("Alerting receiver (webhook/Slack)", "Route alerts to a webhook/Slack channel.", "infra/alertmanager.yml"), + ("Prometheus scrape config", "Scrape config for all service /metrics endpoints.", "infra/prometheus/prometheus.yml"), + ("Uptime/synthetic check for API", "External check hitting /healthz + a real endpoint.", "infra/monitoring/synthetic.md"), +]: + labels = ["area:monitoring", "P1"] + if "Grafana" in title or "Alert" in title or "Prometheus scrape" in title or "synthetic" in title: labels.insert(1, "area:infra") + add(f"[MONITORING] {title}", labels, MS_MONITOR, + "Observability so we can run the backend in production safely.", + what, + ["Implemented per description", "Renders/scrapes correctly in a local Prometheus/Grafana", + "Documented in infra/README.md"], + loc) + + +# ────────────────────────────────────────────────────────────────────────────── +# TESTING (area:testing) — ~20 +# ────────────────────────────────────────────────────────────────────────────── +for title, what, loc, ms in [ + ("Vitest config for backend", "Add vitest config + scripts for the backend package.", "backend/vitest.config.ts", MS_TESTING), + ("Vitest config for indexer", "Add vitest config for the indexer.", "indexer/vitest.config.ts", MS_TESTING), + ("Vitest config for oracle", "Add vitest config for the oracle.", "oracle/vitest.config.ts", MS_TESTING), + ("Test DB bootstrap (ephemeral Postgres)", "Spin up an ephemeral Postgres for integration tests.", "backend/test/db.ts", MS_TESTING), + ("Test Redis bootstrap", "Provide a fake/ephemeral Redis for tests.", "backend/test/redis.ts", MS_TESTING), + ("Fixtures: sample decoded events", "Recorded event fixtures for indexer handler tests.", "indexer/test/fixtures/", MS_TESTING), + ("Indexer handler unit tests (all events)", "Unit-test every event handler against fixtures.", "indexer/test/handlers.test.ts", MS_TESTING), + ("Idempotency replay test", "Replay a ledger batch twice; assert no drift.", "indexer/test/replay.test.ts", MS_TESTING), + ("Backend query-layer unit tests", "Test each DB query function.", "backend/test/db.test.ts", MS_TESTING), + ("Cache layer unit tests", "Test getOrSet, invalidation, limiter.", "backend/test/cache.test.ts", MS_TESTING), + ("API e2e smoke test", "Boot the API and hit each endpoint once.", "backend/test/e2e.test.ts", MS_TESTING), + ("Oracle threshold logic tests", "Unit-test council threshold/outcome selection.", "oracle/test/threshold.test.ts", MS_TESTING), + ("Oracle adapter fixture tests", "Test adapters against recorded responses.", "oracle/test/adapters.test.ts", MS_TESTING), + ("Load test script (markets list)", "k6/autocannon script for the hot endpoint.", "backend/test/load/markets.js", MS_TESTING), + ("Contract: optimistic oracle rust tests", "Rust tests for the optimistic state machine.", "contracts/prediction_market/src/test.rs", MS_TESTING), + ("Test coverage reporting", "Add coverage output to each package's test run.", "backend/vitest.config.ts", MS_TESTING), + ("Error-path tests (DB/Redis down)", "Assert graceful degradation when deps are down.", "backend/test/resilience.test.ts", MS_TESTING), + ("Address/amount util tests", "Unit-test address + stroop/XLM utilities.", "backend/test/utils.test.ts", MS_TESTING), + ("Pagination edge-case tests", "Negative/huge/non-numeric inputs.", "backend/test/pagination.test.ts", MS_TESTING), + ("Seed-data verification test", "Assert seed script produces expected rows.", "db/test/seed.test.ts", MS_TESTING), +]: + labels = ["area:testing"] + if "indexer" in loc: labels.append("area:indexer") + elif "oracle" in loc: labels.append("area:oracle") + elif "contracts" in loc: labels.append("area:contracts") + elif loc.startswith("db/"): labels.append("area:db") + else: labels.append("area:backend") + labels.append("P1") + add(f"[TEST] {title}", labels, ms, + "Test coverage so contributors can refactor safely and we can ship confidently.", + what, + ["Test(s) implemented per description", "Pass locally with `npm test`", "Deterministic (no flakiness)"], + loc) + + +# ────────────────────────────────────────────────────────────────────────────── +# DOCS (area:docs) — ~10 +# ────────────────────────────────────────────────────────────────────────────── +for title, what, loc in [ + ("API reference (endpoints, params, responses)", "Write a complete API reference for all endpoints.", "docs/API.md"), + ("Indexer operations runbook", "How to run, backfill, recover, and monitor the indexer.", "docs/INDEXER_RUNBOOK.md"), + ("Oracle operations runbook", "Council + optimistic operations end to end.", "docs/ORACLE_RUNBOOK.md"), + ("Local development guide", "One-page guide to run the whole stack locally.", "docs/LOCAL_DEV.md"), + ("Deployment guide (backend stack)", "Deploy api/indexer/oracle/db/redis to production.", "docs/BACKEND_DEPLOYMENT.md"), + ("Database schema reference", "Authoritative schema doc with ER diagram.", "docs/DB_SCHEMA.md"), + ("Architecture diagram (current vs target)", "Update ARCHITECTURE.md with the backend/oracle target.", "docs/ARCHITECTURE.md"), + ("Contributor onboarding (where to start)", "A guided path through good starting issues.", "docs/ONBOARDING.md"), + ("Security considerations doc", "Threat model for backend + oracle (keys, bonds, RPC trust).", "docs/SECURITY_BACKEND.md"), + ("Glossary of terms", "Define market/bet/outcome/bond/council/etc.", "docs/GLOSSARY.md"), +]: + add(f"[DOCS] {title}", ["area:docs", "P2"], MS_BACKEND, + "Clear docs so contributors don't get stuck.", + what, + ["Document written and linked from README/CONTRIBUTING", "Accurate to the current code", "Renders cleanly on GitHub"], + loc) + + +# ────────────────────────────────────────────────────────────────────────────── +# EXTRA — round out to 260 with genuinely useful cross-cutting work +# ────────────────────────────────────────────────────────────────────────────── +add("[BACKEND] Shared `@ipredict/shared` types package", + ["area:backend", "P1"], MS_BACKEND, + "Backend, indexer, oracle, and frontend all need the same Market/Bet/event types.", + "Create a small internal shared package exporting common TS types + enums (categories, event " + "topics) consumed by all Node services.", + ["Package builds and is importable by backend/indexer/oracle", + "Single source of truth for shared types", "No circular deps"], + "shared/") + +add("[BACKEND] Root workspace wiring (npm workspaces)", + ["area:backend", "area:infra", "P1"], MS_BACKEND, + "Installing/building each service separately is tedious; wire a workspace.", + "Add a root package.json with npm workspaces for backend/indexer/oracle/shared so a single " + "`npm install` bootstraps all.", + ["`npm install` at root installs all services", "Per-service scripts still work", + "Documented in CONTRIBUTING.md"], + "package.json (root)") + +add("[INDEXER] Token contract balance tracking (optional table)", + ["area:indexer", "area:db", "P2"], MS_BACKEND, + "Profile pages may show IPRED balances; track mint/transfer events.", + "Optionally index token mint/transfer events into a `token_balances` table.", + ["Decides + documents whether to track on-chain or via SAC", "If tracked, balances stay consistent"], + "indexer/src/handlers/token.ts") + +add("[API] GET /api/markets/:id/odds — derived odds endpoint", + ["area:api", "area:backend", "P2"], MS_BACKEND, + "Frontend shows YES/NO odds derived from pool totals.", + "Add an endpoint returning derived odds/implied probability from total_yes/total_no.", + ["Returns yes/no implied probabilities", "Handles zero-pool gracefully", "Tested"], + "backend/src/api/markets.ts") + +add("[ORACLE] Outcome explanation / provenance record", + ["area:oracle", "P2"], MS_DATA, + "For trust, store why an outcome was chosen (which sources, values).", + "Persist a provenance record per resolved market: sources queried, raw values, decision.", + ["Provenance stored per resolution", "Retrievable for audit/display", "No secrets in provenance"], + "oracle/src/adapters/provenance.ts") + +add("[MONITORING] Status page data feed", + ["area:monitoring", "area:backend", "P2"], MS_MONITOR, + "A public status feed builds user trust.", + "Expose a small JSON status feed (indexer lag, last resolved market, API health) for a status page.", + ["Single endpoint summarizing health", "No sensitive data", "Cacheable"], + "backend/src/api/status.ts") + +add("[TEST] CI-less local verify script (typecheck+test all)", + ["area:testing", "area:infra", "P1"], MS_TESTING, + "With no CI on this branch, contributors need one command to self-check before PR.", + "Add a `verify` script that runs typecheck + tests across all Node services.", + ["One command checks backend+indexer+oracle", "Non-zero exit on any failure", + "Documented in CONTRIBUTING.md"], + "scripts/verify-all.sh") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--dry-run", action="store_true", help="print titles + counts only") + ap.add_argument("--sample", type=int, default=0, help="create only the first N issues") + args = ap.parse_args() + + total = len(ISSUES) + # counts by primary area label + from collections import Counter + by_area = Counter() + for _, labels, _, _ in ISSUES: + area = next((l for l in labels if l.startswith("area:")), "area:?") + by_area[area] += 1 + + print(f"TOTAL ISSUES: {total}") + for area, c in sorted(by_area.items()): + print(f" {area:18} {c}") + print() + + if args.dry_run: + for i, (title, labels, ms, _) in enumerate(ISSUES, 1): + print(f"{i:3}. {title} [{','.join(labels)}] ms={ms}") + return + + # fetch existing titles to skip duplicates + existing = set() + try: + out = subprocess.run( + ["gh", "issue", "list", "--repo", REPO, "--state", "all", + "--limit", "1000", "--json", "title"], + capture_output=True, text=True, check=True).stdout + existing = {x["title"] for x in json.loads(out)} + except Exception as e: + print(f"warn: could not fetch existing issues: {e}", file=sys.stderr) + + to_create = ISSUES[: args.sample] if args.sample else ISSUES + created = skipped = failed = 0 + for i, (title, labels, ms, bdy) in enumerate(to_create, 1): + if title in existing: + print(f"skip (exists): {title}") + skipped += 1 + continue + cmd = ["gh", "issue", "create", "--repo", REPO, "--title", title, + "--body", bdy, "--milestone", str(ms)] + for l in labels: + cmd += ["--label", l] + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode == 0: + print(f"[{i}/{len(to_create)}] created: {title} -> {r.stdout.strip()}") + created += 1 + else: + print(f"FAIL: {title}\n {r.stderr.strip()}", file=sys.stderr) + failed += 1 + time.sleep(0.6) + + print(f"\n---- created={created} skipped={skipped} failed={failed} ----") + + +if __name__ == "__main__": + main()