diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..1c66f13b --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# EVODRON repository entrypoint +# +# The runnable product now lives in /evodron. +# Copy this file to .env at the repository root or to evodron/.env. + +# Secret used to sign JWTs. Use a long random value (32+ chars). +EVODRON_JWT_SECRET=change-me-to-a-long-random-secret + +# API bind address. +EVODRON_API_HOST=0.0.0.0 +EVODRON_API_PORT=3000 + +# API URL used by the CLI. +EVODRON_API_URL=http://localhost:3000 + +# SQLite path for local development. +EVODRON_DB_URL=file:./evodron.db + +# Public base URL used to build referral links. +EVODRON_REFERRAL_BASE_URL=http://localhost:3000 + +# Minimum completed sessions before a referral is rewarded. +EVODRON_REWARD_MIN_SESSIONS=3 + +# Optional comma-separated CORS allowlist. Leave unset for development. +# EVODRON_CORS_ORIGIN=https://app.evodron.io,https://admin.evodron.io + +# Max JSON request size in bytes. +EVODRON_BODY_LIMIT_BYTES=1048576 + +NODE_ENV=development +LOG_LEVEL=info diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..10a8abb0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + working-directory: evodron + run: npm ci + + - name: Build dependencies + working-directory: evodron + run: npm run build -w @evodron/shared -w @evodron/db + + - name: Lint workspace + working-directory: evodron + run: npm run lint + + - name: Run tests + env: + EVODRON_JWT_SECRET: test-secret-ci-secret-ci-secret-ci + EVODRON_DB_URL: file:/tmp/evodron-ci.db + NODE_ENV: test + working-directory: evodron + run: npm test + + - name: Build workspace + working-directory: evodron + run: npm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5f7e2b7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +evodron.db +*.db +.env diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..242ef9c6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Guide de contribution — Evodron + +Merci de contribuer à Evodron ! Voici les règles à suivre. + +## Pré-requis + +- Node.js >= 18 +- npm >= 9 + +## Installation locale + +```bash +git clone https://github.com/julesdemangeot-ship-it/EVODRON.git +cd EVODRON +npm install +cp .env.example .env +# Éditer .env et définir JWT_SECRET +``` + +## Lancer les tests + +```bash +npm test +``` + +Les tests utilisent une base SQLite en mémoire. Aucune configuration externe requise. + +## Conventions + +- **Code** : `'use strict'` en tête de chaque fichier JS. +- **Commits** : messages en anglais, préfixés (`feat:`, `fix:`, `chore:`, `docs:`, `test:`). +- **Branches** : `feature/`, `fix/`, `chore/`. +- **Tests** : toute nouvelle fonctionnalité doit être accompagnée de tests dans `tests/`. + +## Structure du code EVODRON + +Voir `EVODRON.md` pour la description complète de l'architecture. + +## Pull Requests + +1. Créer une branche depuis `main`. +2. Écrire ou adapter les tests. +3. S'assurer que `npm test` passe à 100%. +4. Ouvrir une PR avec une description claire. + +## Sécurité + +Voir `SECURITY.md` pour signaler une vulnérabilité. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e69f3bac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM node:20-alpine AS builder + +WORKDIR /app/evodron + +COPY evodron/package*.json ./ +COPY evodron/packages/api/package.json ./packages/api/package.json +COPY evodron/packages/cli/package.json ./packages/cli/package.json +COPY evodron/packages/db/package.json ./packages/db/package.json +COPY evodron/packages/shared/package.json ./packages/shared/package.json + +RUN npm ci + +COPY evodron/ ./ + +RUN npm run build + +FROM node:20-alpine AS runtime + +WORKDIR /app/evodron + +COPY evodron/package*.json ./ +COPY evodron/packages/api/package.json ./packages/api/package.json +COPY evodron/packages/cli/package.json ./packages/cli/package.json +COPY evodron/packages/db/package.json ./packages/db/package.json +COPY evodron/packages/shared/package.json ./packages/shared/package.json + +RUN npm ci --omit=dev + +COPY --from=builder /app/evodron/packages/api/dist ./packages/api/dist +COPY --from=builder /app/evodron/packages/db/dist ./packages/db/dist +COPY --from=builder /app/evodron/packages/shared/dist ./packages/shared/dist + +# VOLUME ["/data"] +ENV EVODRON_DB_URL=file:/data/evodron.db +ENV EVODRON_API_HOST=0.0.0.0 +ENV EVODRON_API_PORT=3000 + +EXPOSE 3000 + +USER node + +CMD ["sh", "-c", "npm run db:migrate && npm run start --workspace=@evodron/api"] diff --git a/EVODRON.md b/EVODRON.md new file mode 100644 index 00000000..25815066 --- /dev/null +++ b/EVODRON.md @@ -0,0 +1,145 @@ +# Evodron + +**Plateforme EVODRON** — API TypeScript/Fastify, CLI EVODRON, base SQLite et architecture modulaire. + +> ℹ️ Le fichier `README.md` à la racine documente le **GitHub Copilot CLI** hérité du dépôt. Le produit EVODRON réellement exécutable vit désormais dans le dossier `/evodron` et est piloté depuis la racine via les scripts npm de ce dépôt. + +--- + +## Architecture + +``` +/ +├── package.json # Scripts racine (setup / build / test / start) +├── Dockerfile # Image API EVODRON +├── docker-compose.yml # Déploiement local/simple +└── evodron/ + ├── package.json # Workspace npm + ├── docs/ # Documentation produit / architecture + ├── scripts/setup.sh # Setup développeur + └── packages/ + ├── api/ # API Fastify + auth + referrals + rewards + ├── cli/ # CLI EVODRON + ├── db/ # Schéma, migrations, seed + └── shared/ # Types et schémas partagés +``` + +## Prérequis + +- **Node.js** >= 18 +- **npm** >= 9 + +## Installation + +```bash +cd /home/runner/work/EVODRON/EVODRON +cp .env.example .env +# Éditer .env et définir EVODRON_JWT_SECRET +npm run setup +``` + +## Démarrage + +```bash +npm start +``` + +Le serveur écoute sur `http://localhost:3000` (configurable via `EVODRON_API_PORT`). + +## Tests + +```bash +npm test +``` + +La suite lance les tests workspace EVODRON (`evodron/packages/*`). + +## API + +### Authentification + +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| `POST` | `/auth/register` | Créer un compte (avec code de parrainage optionnel) | +| `POST` | `/auth/login` | Se connecter, obtenir access token + refresh token | +| `POST` | `/auth/refresh` | Renouveler la session | +| `POST` | `/auth/logout` | Révoquer un refresh token | + +### Parrainage + +| Méthode | Endpoint | Auth | Description | +|---------|----------|------|-------------| +| `GET` | `/users/me` | ✅ | Profil courant | +| `GET` | `/referrals/code` | ✅ | Obtenir son code et lien | +| `GET` | `/referrals/stats` | ✅ | Statistiques de parrainage | +| `POST` | `/sessions/start` | ✅ | Démarrer une session CLI | +| `POST` | `/sessions/end` | ✅ | Terminer une session CLI | +| `GET` | `/rewards` | ✅ | Historique des crédits | +| `GET` | `/rewards/rules` | ✅ | Règles actives | +| `GET` | `/stats` | ✅ | Statistiques d’utilisation | + +### Santé + +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| `GET` | `/health` | État de l'application et de la base de données | + +### Authentification JWT + +Passer l’access token JWT via le header HTTP `Authorization: ****** + +## Variables d'environnement + +Voir `.env.example` pour la liste complète. + +| Variable | Obligatoire | Défaut | Description | +|----------|-------------|--------|-------------| +| `EVODRON_JWT_SECRET` | ✅ | — | Clé secrète JWT | +| `EVODRON_API_PORT` | ❌ | `3000` | Port d'écoute | +| `EVODRON_API_HOST` | ❌ | `0.0.0.0` | Host d'écoute | +| `EVODRON_API_URL` | ❌ | `http://localhost:3000` | URL de l’API pour le CLI | +| `EVODRON_REFERRAL_BASE_URL` | ❌ | `https://evodron.io` | URL de base des liens de parrainage | +| `EVODRON_DB_URL` | ❌ | `file:./evodron.db` | Chemin/URL de base SQLite | +| `EVODRON_REWARD_MIN_SESSIONS` | ❌ | `3` | Seuil avant validation d’un parrainage | +| `EVODRON_BODY_LIMIT_BYTES` | ❌ | `1048576` | Taille max des payloads JSON | +| `NODE_ENV` | ❌ | `development` | Environnement d'exécution | +| `LOG_LEVEL` | ❌ | `info` | Niveau de logs | + +## Déploiement Docker + +```bash +# Copier et éditer les variables +cp .env.example .env +# Démarrer +docker compose up -d +``` + +## Flux de parrainage + +``` +Alice crée son compte → obtient un code de parrainage unique + ↓ +Alice partage son lien : /ref/ABCD1234 + ↓ +Bob s'inscrit avec ce code → reçoit un bonus de bienvenue (50 crédits) + ↓ +Bob complète le nombre minimum de sessions configuré + ↓ +Alice reçoit sa récompense de parrain (100 crédits par défaut) +``` + +## Sécurité + +- Mots de passe hachés avec **bcrypt** (coût 12) +- Access tokens courts + refresh tokens révoquables +- Rate limiting global et spécifique auth +- Auto-parrainage interdit +- Un utilisateur ne peut avoir qu'un seul parrain +- IP hachées pour l’anti-abus +- Pas de récompense sans usage vérifié + +## Licence + +Le code source EVODRON exécutable se trouve dans `evodron/` et est propriétaire. + +Le fichier `README.md`, `install.sh`, `changelog.md` et `LICENSE.md` sont des fichiers hérités du **GitHub Copilot CLI** distribués sous leur propre licence (voir `LICENSE.md`). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..32c34018 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Politique de sécurité — Evodron + +## Signaler une vulnérabilité + +Si vous découvrez une vulnérabilité de sécurité dans Evodron, **ne l'ouvrez pas publiquement** en tant qu'issue GitHub. + +Contactez directement le mainteneur via la fonctionnalité **"Report a vulnerability"** de GitHub (onglet "Security" du dépôt) ou par email privé. + +Nous nous engageons à : +- Accuser réception sous 48 heures. +- Fournir un correctif ou un plan d'action sous 7 jours pour les vulnérabilités critiques. +- Créditer le rapporteur dans le changelog si souhaité. + +## Versions supportées + +| Version | Support sécurité | +|---------|-----------------| +| 1.x | ✅ Oui | + +## Bonnes pratiques intégrées + +- Mots de passe hachés avec **bcrypt** (coût 12) +- Tokens JWT signés HS256 avec expiration +- Rate limiting sur toutes les routes d'authentification +- Protection anti-auto-parrainage (modèle + contrainte DB) +- Variables sensibles jamais commitées (`.env` dans `.gitignore`) diff --git a/app.js b/app.js new file mode 100644 index 00000000..f2704645 --- /dev/null +++ b/app.js @@ -0,0 +1,46 @@ +'use strict'; + +const express = require('express'); +const cookieParser = require('cookie-parser'); +const path = require('path'); + +const authRoutes = require('./src/routes/auth'); +const referralRoutes = require('./src/routes/referral'); +const healthRoutes = require('./src/routes/health'); +const { authLimiter, apiLimiter } = require('./src/middleware/rateLimiter'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); + +// Serve static frontend files +app.use(express.static(path.join(__dirname, 'public'))); + +// Health check (no rate limit) +app.use('/api/health', healthRoutes); + +// API routes +app.use('/api/auth', authLimiter, authRoutes); +app.use('/api/referral', apiLimiter, referralRoutes); + +// Catch-all: serve index.html for client-side navigation +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// Only start listening when run directly (not during tests) +if (require.main === module) { + if (!process.env.JWT_SECRET) { + console.error('ERROR: JWT_SECRET environment variable is not set. Refusing to start.'); + process.exit(1); + } + app.listen(PORT, () => { + console.log(`Evodron Referral server running on http://localhost:${PORT}`); + }); +} + +module.exports = app; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b5a0a093 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + api: + build: . + ports: + - "3000:3000" + environment: + - EVODRON_JWT_SECRET=${EVODRON_JWT_SECRET:?EVODRON_JWT_SECRET is required} + - EVODRON_API_HOST=0.0.0.0 + - EVODRON_API_PORT=3000 + - EVODRON_DB_URL=file:/data/evodron.db + - EVODRON_API_URL=${EVODRON_API_URL:-http://localhost:3000} + - EVODRON_REFERRAL_BASE_URL=${EVODRON_REFERRAL_BASE_URL:-http://localhost:3000} + - EVODRON_REWARD_MIN_SESSIONS=${EVODRON_REWARD_MIN_SESSIONS:-3} + - NODE_ENV=${NODE_ENV:-production} + - LOG_LEVEL=${LOG_LEVEL:-info} + volumes: + - evodron_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +volumes: + evodron_data: diff --git a/evodron/.env.example b/evodron/.env.example new file mode 100644 index 00000000..bd1f0f55 --- /dev/null +++ b/evodron/.env.example @@ -0,0 +1,49 @@ +# EVODRON — Environment Variables + +# Copy this file to .env and fill in real values. +# NEVER commit .env to version control. + +# ─── API Server ────────────────────────────────────────────────────────────── + +# Secret used to sign JWTs. Must be a long, random string (32+ chars). +EVODRON_JWT_SECRET=change-me-to-a-long-random-secret + +# Port the API listens on. +EVODRON_API_PORT=3000 + +# Host the API binds to. +EVODRON_API_HOST=0.0.0.0 + +# ─── Database ──────────────────────────────────────────────────────────────── + +# SQLite: file path (development / small production) +EVODRON_DB_URL=file:./evodron.db + +# PostgreSQL: connection string (large production) +# EVODRON_DB_URL=******host:5432/evodron + +# ─── Referral System ───────────────────────────────────────────────────────── + +# Public base URL for referral links (no trailing slash). +EVODRON_REFERRAL_BASE_URL=https://evodron.io + +# Minimum number of completed sessions before a referral is considered "active" +# and rewards are triggered. +EVODRON_REWARD_MIN_SESSIONS=3 + +# ─── CLI ───────────────────────────────────────────────────────────────────── + +# URL of the EVODRON API that the CLI communicates with. +EVODRON_API_URL=http://localhost:3000 + +# Path to the local config directory (defaults to ~/.evodron if not set). +# EVODRON_CONFIG_DIR=~/.evodron + +# ─── Environment ───────────────────────────────────────────────────────────── + +NODE_ENV=development + +# ─── Logging ───────────────────────────────────────────────────────────────── + +# Log level: trace | debug | info | warn | error | fatal +LOG_LEVEL=info diff --git a/evodron/.gitignore b/evodron/.gitignore new file mode 100644 index 00000000..49281bee --- /dev/null +++ b/evodron/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +/packages/*/src/*.js +/packages/*/src/*.d.ts +/packages/*/src/*.js.map +/packages/*/src/*.d.ts.map +*.js.map +.env +*.db +*.db-journal +*.log +coverage/ +.nyc_output/ diff --git a/evodron/docs/ARCHITECTURE.md b/evodron/docs/ARCHITECTURE.md new file mode 100644 index 00000000..b1ce1927 --- /dev/null +++ b/evodron/docs/ARCHITECTURE.md @@ -0,0 +1,102 @@ +# EVODRON Architecture + +## Overview + +EVODRON is an AI-powered developer platform that orchestrates the GitHub Copilot CLI to deliver a richer, identity-branded experience with user accounts, referral programmes, reward credits, and usage statistics. + +``` +┌──────────────────────────────────────────────────────────┐ +│ USER │ +│ evodron │ +└──────────────────────┬───────────────────────────────────┘ + │ +┌──────────────────────▼───────────────────────────────────┐ +│ EVODRON CLI WRAPPER (packages/cli) │ +│ • Splash screen / branding │ +│ • Local config (~/.evodron/config.json) │ +│ • Auth token management │ +│ • Usage metrics (local, with consent) │ +│ • Delegates to GitHub Copilot CLI for AI tasks │ +└──────┬───────────────────────────────────────────────────┘ + │ HTTPS (JWT) +┌──────▼───────────────────────────────────────────────────┐ +│ EVODRON API (packages/api) │ +│ │ +│ Routes: │ +│ POST /auth/register — create account │ +│ POST /auth/login — get JWT + refresh token │ +│ POST /auth/refresh — rotate JWT │ +│ GET /users/me — profile + credits │ +│ GET /referrals/code — my referral code/link │ +│ GET /referrals/stats — referrals dashboard │ +│ POST /sessions/start — log session start │ +│ POST /sessions/end — log session end │ +│ GET /rewards — reward history │ +│ GET /stats — usage statistics │ +│ GET /admin/abuse-flags — abuse management (admin) │ +│ │ +│ Middleware: │ +│ • JWT authentication │ +│ • Rate limiting (per IP + per user) │ +│ • Anti-abuse detection │ +└──────┬───────────────────────────────────────────────────┘ + │ Drizzle ORM +┌──────▼───────────────────────────────────────────────────┐ +│ DATABASE (packages/db) │ +│ │ +│ SQLite (development / small production) │ +│ PostgreSQL (large production — swap DB_URL) │ +│ │ +│ Tables: │ +│ users — accounts, referral codes │ +│ sessions — usage tracking per user │ +│ referrals — parrain→filleul relationships │ +│ rewards — credit ledger │ +│ reward_rules — configurable reward triggers │ +│ abuse_flags — fraud/abuse detection records │ +│ ip_registry — IP tracking for anti-fraud │ +└──────────────────────────────────────────────────────────┘ +``` + +## Packages + +### `packages/shared` +TypeScript types and Zod schemas shared between CLI and API packages. Zero runtime dependencies beyond `zod`. + +### `packages/db` +Drizzle ORM schema definitions and migrations. Supports SQLite (via `better-sqlite3`) in development and small deployments, and PostgreSQL (via `postgres`) in production — controlled by `DB_URL`. + +### `packages/api` +Fastify-based REST API. Handles authentication (JWT), referrals, rewards, sessions, stats, and abuse detection. All routes are rate-limited. Auth-required routes validate JWT on every request. + +### `packages/cli` +The `evodron` CLI wrapper. Provides the branded entry point. Handles local config, authentication against the API, and delegates AI coding tasks to the GitHub Copilot CLI binary. Collects opt-in usage metrics. + +## Security considerations + +- Passwords are hashed with bcrypt (cost factor 12). +- JWTs expire after 15 minutes; refresh tokens after 30 days. +- Refresh tokens are stored hashed in the database; rotation invalidates the old token. +- Rate limiting: 100 req/min per IP globally; 20 req/min on auth endpoints. +- Anti-abuse: IP deduplication, self-referral prevention, minimum activity threshold before reward. +- No API keys, secrets, or passwords are ever stored in the codebase. + +## Referral system + +1. Each registered user receives a unique 8-character alphanumeric referral code. +2. A shareable link is derived from `EVODRON_REFERRAL_BASE_URL/ref/`. +3. When a new user registers with `?ref=`, the `referrals` row is created with status `pending`. +4. After the referee reaches `EVODRON_REWARD_MIN_SESSIONS` active sessions, status becomes `active`. +5. Reward credits are credited to both referee and referrer via the configurable `reward_rules`. + +## Reward system (virtual credits only) + +- Credits are a **virtual, internal unit** with no monetary value. +- Reward rules are stored in the `reward_rules` table and can be updated without code changes. +- Default rules: referrer receives 100 credits per active referee; referee receives 50 onboarding credits. +- No cash-out, fiat conversion, or financial features are implemented. +- Any future financial feature would require legal, fiscal, and payment compliance work first. + +## EVODRON is not affiliated with GitHub or Microsoft + +The GitHub Copilot CLI is a third-party binary component. See `THIRD_PARTY.md`. diff --git a/evodron/docs/EVODRON.md b/evodron/docs/EVODRON.md new file mode 100644 index 00000000..b3f83ff7 --- /dev/null +++ b/evodron/docs/EVODRON.md @@ -0,0 +1,177 @@ +# EVODRON + +EVODRON is an AI-powered developer platform that enriches the GitHub Copilot CLI experience with user accounts, a referral programme, reward credits, usage statistics, and anti-abuse protection. + +> **Disclaimer:** EVODRON is not affiliated with, endorsed by, or officially connected to GitHub, Inc. or Microsoft Corporation. The GitHub Copilot CLI is a separate, third-party binary component. See [THIRD_PARTY.md](./THIRD_PARTY.md). + +--- + +## Features + +- **Evodron identity** — branded CLI with splash screen and EVODRON visual identity +- **User accounts** — registration, login, JWT authentication, profile management +- **Referral programme** — unique codes, shareable links, tracking, and anti-abuse +- **Reward credits** — virtual credits with configurable rules (no monetary value) +- **Usage statistics** — sessions, commands, credit history, level system +- **Anti-abuse** — IP tracking, self-referral prevention, fraud detection, rate limiting +- **Admin tools** — manage abuse flags, ban users, configure reward rules + +--- + +## Quick Start + +### Prerequisites + +- Node.js >= 18 +- (Optional) GitHub Copilot CLI for AI features — install with `curl -fsSL https://gh.io/copilot-install | bash` + +### Setup + +```bash +cd evodron +bash scripts/setup.sh +``` + +Then edit `.env` to set a secure `EVODRON_JWT_SECRET`. + +### Start the API + +```bash +npm run dev:api +``` + +### Build and use the CLI + +```bash +npm run build +node packages/cli/dist/bin/evodron.js --help +``` + +### Register an account + +```bash +evodron auth register +``` + +### Launch AI assistant (delegates to GitHub Copilot CLI) + +```bash +evodron run -- "explain this code" +``` + +--- + +## CLI Commands + +| Command | Description | +|---------|-------------| +| `evodron auth register` | Create a new EVODRON account | +| `evodron auth login` | Log in | +| `evodron auth logout` | Log out | +| `evodron auth status` | Show login status | +| `evodron referral code` | Show your referral code and link | +| `evodron referral stats` | Show referral dashboard | +| `evodron rewards history` | Show reward credit history | +| `evodron rewards rules` | Show active reward rules | +| `evodron stats` | Show usage statistics | +| `evodron run [args...]` | Launch GitHub Copilot CLI (third-party) | + +--- + +## API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/health` | No | Health check | +| POST | `/auth/register` | No | Register | +| POST | `/auth/login` | No | Login | +| POST | `/auth/refresh` | No | Refresh token | +| POST | `/auth/logout` | No | Logout | +| GET | `/users/me` | Yes | Get profile | +| PATCH | `/users/me/consent` | Yes | Update metrics consent | +| GET | `/referrals/code` | Yes | Get referral code | +| GET | `/referrals/stats` | Yes | Referral dashboard | +| POST | `/sessions/start` | Yes | Start session | +| POST | `/sessions/end` | Yes | End session | +| GET | `/rewards` | Yes | Reward history | +| GET | `/rewards/rules` | Yes | Active reward rules | +| GET | `/stats` | Yes | Usage stats | +| GET | `/admin/abuse-flags` | Admin | Unresolved abuse flags | +| POST | `/admin/abuse-flags/:id/resolve` | Admin | Resolve flag | +| POST | `/admin/users/:id/ban` | Admin | Ban user | +| GET | `/admin/reward-rules` | Admin | All reward rules | +| PATCH | `/admin/reward-rules/:id` | Admin | Update reward rule | + +--- + +## Referral Programme + +Each user receives a unique 8-character referral code and a shareable link. + +When a new user registers using a referral link: +1. A `pending` referral is created linking referrer and referee. +2. The referee receives onboarding credits immediately. +3. After the referee completes `EVODRON_REWARD_MIN_SESSIONS` sessions, the referral becomes `active`. +4. The referrer receives referral credits. + +**Anti-abuse measures:** +- Self-referral is blocked (a user cannot use their own code). +- Duplicate referrals are prevented (one referee → one referrer only). +- IP tracking flags accounts created from the same IP address excessively. +- Banned users' pending referrals are marked as fraud. +- Rewards only trigger after verified usage (minimum sessions threshold). + +--- + +## Reward System + +EVODRON credits are **virtual, internal units with no monetary value**. They cannot be exchanged for cash, goods, or services. + +| Level | Credits required | +|-------|-----------------| +| Bronze | 0 | +| Silver | 300 | +| Gold | 1 000 | + +Default reward rules: + +| Rule | Trigger | Credits | +|------|---------|---------| +| Referral (referrer) | Referee reaches active status | 100 | +| Onboarding (referee) | Join via referral link | 50 | +| Milestone | Complete 10 sessions | 25 | + +Rules are configurable via `PATCH /admin/reward-rules/:id` without code changes. + +> ⚠️ **Legal notice:** Any future conversion of credits to real money would require legal, fiscal, and payment compliance work (payment institution registration, KYC, GDPR compliance, tax obligations). This is intentionally **not implemented**. + +--- + +## Environment Variables + +See `.env.example` for all available variables. + +| Variable | Required | Description | +|----------|----------|-------------| +| `EVODRON_JWT_SECRET` | Yes | JWT signing secret (32+ chars) | +| `EVODRON_DB_URL` | Yes | SQLite path or PostgreSQL URL | +| `EVODRON_API_URL` | CLI | API URL (used by CLI) | +| `EVODRON_REFERRAL_BASE_URL` | No | Base URL for referral links | +| `EVODRON_REWARD_MIN_SESSIONS` | No | Minimum sessions before referral activates (default: 3) | +| `EVODRON_API_PORT` | No | API port (default: 3000) | +| `NODE_ENV` | No | `development` or `production` | +| `LOG_LEVEL` | No | Log level (default: `info`) | + +--- + +## Architecture + +See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full architecture diagram and package descriptions. + +--- + +## Third-Party Components + +See [THIRD_PARTY.md](./THIRD_PARTY.md) for full attribution. + +The GitHub Copilot CLI is a third-party binary component by GitHub, Inc. EVODRON uses it as an optional, unmodified sub-component for AI coding features. diff --git a/evodron/docs/THIRD_PARTY.md b/evodron/docs/THIRD_PARTY.md new file mode 100644 index 00000000..80143935 --- /dev/null +++ b/evodron/docs/THIRD_PARTY.md @@ -0,0 +1,49 @@ +# Third-Party Component Attribution + +EVODRON includes or depends on the following third-party components. + +--- + +## GitHub Copilot CLI + +- **Component**: GitHub Copilot CLI binary (`gh copilot` / `ghcs`) +- **Source**: https://github.com/github/copilot-cli +- **Author**: GitHub, Inc. (a subsidiary of Microsoft Corporation) +- **License**: GitHub Copilot CLI License (see `../LICENSE.md`) + +### Important notices + +- EVODRON is **not affiliated with, endorsed by, or officially connected to GitHub, Inc. or Microsoft Corporation**. +- The GitHub Copilot CLI is a **separate, unmodified binary** component redistributed as part of EVODRON in accordance with Section 2 of the GitHub Copilot CLI License. +- EVODRON does **not** modify, adapt, translate, or create derivative works of the GitHub Copilot CLI. +- EVODRON does **not** claim authorship of the GitHub Copilot CLI. +- The GitHub Copilot CLI is distributed **only in unmodified form** and is not the primary product of EVODRON; it is one component among others. +- All copyright, trademark, and attribution notices from the GitHub Copilot CLI are retained. + +The EVODRON platform — including its CLI wrapper, backend API, referral system, reward system, user accounts, and all other custom code — is original work independent of the GitHub Copilot CLI. + +--- + +## Open Source Dependencies + +The following open-source packages are used by EVODRON's original codebase. Each is used under its respective license. + +| Package | License | Usage | +|---------|---------|-------| +| fastify | MIT | HTTP API server | +| @fastify/jwt | MIT | JWT authentication plugin | +| @fastify/rate-limit | MIT | Rate limiting | +| @fastify/cors | MIT | CORS headers | +| drizzle-orm | Apache-2.0 | Database ORM | +| better-sqlite3 | MIT | SQLite driver | +| commander | MIT | CLI argument parsing | +| chalk | MIT | Terminal color output | +| nanoid | MIT | Unique ID generation | +| bcryptjs | MIT | Password hashing | +| zod | MIT | Schema validation | +| dotenv | BSD-2-Clause | Environment variable loading | +| vitest | MIT | Unit and integration testing | +| tsx | MIT | TypeScript execution | +| typescript | Apache-2.0 | TypeScript compiler | + +Full license texts for these packages are available in each package's `node_modules` directory or at the respective project repositories. diff --git a/evodron/package-lock.json b/evodron/package-lock.json new file mode 100644 index 00000000..f1b58de1 --- /dev/null +++ b/evodron/package-lock.json @@ -0,0 +1,5363 @@ +{ + "name": "evodron", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "evodron", + "version": "0.1.0", + "license": "UNLICENSED", + "workspaces": [ + "packages/*" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@evodron/api": { + "resolved": "packages/api", + "link": true + }, + "node_modules/@evodron/cli": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@evodron/db": { + "resolved": "packages/db", + "link": true + }, + "node_modules/@evodron/shared": { + "resolved": "packages/shared", + "link": true + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/jwt": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.2.1.tgz", + "integrity": "sha512-8FHalB082a6HFj+neP9TdJZrUa65Aq6mtozVjd3vH3Yy7hmjRJUDKLPdxYRYF9S077JeN9RgimsautqnijbDlQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.2.0", + "@lukeed/ms": "^2.0.2", + "fast-jwt": "^6.2.4", + "fastify-plugin": "^6.0.0", + "steed": "^1.1.3" + } + }, + "node_modules/@fastify/jwt/node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/rate-limit": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz", + "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@hono/zod-validator": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.2.2.tgz", + "integrity": "sha512-dSDxaPV70Py8wuIU2QNpoVEIOSzSXZ/6/B/h4xA7eOMz7+AarKTSGV8E6QwrdcCbBLkpqfJ4Q2TmBO0eP1tCBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "hono": ">=3.9.0", + "zod": "^3.19.1" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", + "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "dev": true, + "dependencies": { + "heap": ">= 0.2.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dreamopt": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", + "integrity": "sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==", + "dev": true, + "dependencies": { + "wordwrap": ">=0.0.2" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/drizzle-kit": { + "version": "0.20.18", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.20.18.tgz", + "integrity": "sha512-fLTwcnLqtBxGd+51H/dEm9TC0FW6+cIX/RVPyNcitBO77X9+nkogEfMAJebpd/8Yl4KucmePHRYRWWvUlW0rqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/esm-loader": "^2.5.5", + "@hono/node-server": "^1.9.0", + "@hono/zod-validator": "^0.2.0", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "commander": "^9.4.1", + "env-paths": "^3.0.0", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0", + "glob": "^8.1.0", + "hanji": "^0.0.5", + "hono": "^4.1.4", + "json-diff": "0.9.0", + "minimatch": "^7.4.3", + "semver": "^7.5.4", + "superjson": "^2.2.1", + "zod": "^3.20.2" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-kit/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-jwt": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.3.2.tgz", + "integrity": "sha512-JTQImpkXVvj+eq7tJImtsHRt1K6ngloEzIx62Qbf9x4tEM2P2EqGpYJSeUoPf/kMn78rImSHfhf08KShR8PauA==", + "license": "Apache-2.0", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.40.0", + "safe-regex2": "^5.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fast-jwt/node_modules/mnemonist": { + "version": "0.40.4", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.4.tgz", + "integrity": "sha512-ZAv+KNavneRVzu4tUeOgzkScI3W5BGwZ3rkxIpKtzzVgfTtWQFN1CgX0U72cyvyh3iTuHL3SiSmrQxTlryEIcw==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz", + "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastify": { + "version": "5.11.3", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.11.3.tgz", + "integrity": "sha512-W6hzDP8s0iSeL7LGwY6Oc/ZxuXWOvFEMs6p2L0Si415YRo27W5pBKdOTXxhemBDeSTAcpYf5evRA9onF2OYhPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fastseries": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.1.tgz", + "integrity": "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hanji": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/hanji/-/hanji-0.0.5.tgz", + "integrity": "sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash.throttle": "^4.1.1", + "sisteransi": "^1.0.5" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/json-diff": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.9.0.tgz", + "integrity": "sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-color": "^2.0.0", + "difflib": "~0.2.1", + "dreamopt": "~0.8.0" + }, + "bin": { + "json-diff": "bin/json-diff.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "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/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/steed": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", + "license": "MIT", + "dependencies": { + "fastfall": "^1.5.0", + "fastparallel": "^2.2.0", + "fastq": "^1.3.0", + "fastseries": "^1.7.0", + "reusify": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "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": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/api": { + "name": "@evodron/api", + "version": "0.1.0", + "dependencies": { + "@evodron/db": "*", + "@evodron/shared": "*", + "@fastify/cors": "^10.1.0", + "@fastify/jwt": "^10.2.1", + "@fastify/rate-limit": "^10.3.0", + "bcryptjs": "^2.4.3", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.45.2", + "fastify": "^5.9.0", + "nanoid": "^5.0.4", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/node": "^20.11.25", + "tsx": "^4.7.1", + "typescript": "^5.3.3", + "vitest": "^4.1.10" + } + }, + "packages/cli": { + "name": "@evodron/cli", + "version": "0.1.0", + "dependencies": { + "@evodron/shared": "*", + "chalk": "^4.1.2", + "commander": "^12.0.0", + "dotenv": "^16.4.5", + "node-fetch": "^3.3.2" + }, + "bin": { + "evodron": "dist/bin/evodron.js" + }, + "devDependencies": { + "@types/node": "^20.11.25", + "tsx": "^4.7.1", + "typescript": "^5.3.3", + "vitest": "^4.1.10" + } + }, + "packages/cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "packages/db": { + "name": "@evodron/db", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^9.4.3", + "drizzle-orm": "^0.45.2", + "nanoid": "^5.0.4" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.8", + "drizzle-kit": "^0.20.14", + "tsx": "^4.7.1", + "typescript": "^5.3.3" + } + }, + "packages/shared": { + "name": "@evodron/shared", + "version": "0.1.0", + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "typescript": "^5.3.3" + } + } + } +} diff --git a/evodron/package.json b/evodron/package.json new file mode 100644 index 00000000..67a434b6 --- /dev/null +++ b/evodron/package.json @@ -0,0 +1,29 @@ +{ + "name": "evodron", + "version": "0.1.0", + "description": "EVODRON — AI-powered developer platform built around the GitHub Copilot CLI", + "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "npm run build --workspace=@evodron/shared && npm run build --workspace=@evodron/db && npm run build --workspace=@evodron/api && npm run build --workspace=@evodron/cli", + "test": "npm run test --workspaces --if-present", + "lint": "npm run build --workspace=@evodron/shared && npm run build --workspace=@evodron/db && npm run lint --workspaces --if-present", + "dev:api": "npm run dev --workspace=packages/api", + "dev:cli": "npm run dev --workspace=packages/cli", + "db:migrate": "npm run migrate --workspace=packages/db", + "db:seed": "npm run seed --workspace=packages/db" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "evodron", + "ai", + "cli", + "developer-tools" + ], + "author": "EVODRON", + "license": "UNLICENSED" +} diff --git a/evodron/packages/api/:memory:?cache=shared b/evodron/packages/api/:memory:?cache=shared new file mode 100644 index 00000000..2f7292d0 Binary files /dev/null and b/evodron/packages/api/:memory:?cache=shared differ diff --git a/evodron/packages/api/:memory:?cache=shared-shm b/evodron/packages/api/:memory:?cache=shared-shm new file mode 100644 index 00000000..fe9ac284 Binary files /dev/null and b/evodron/packages/api/:memory:?cache=shared-shm differ diff --git a/evodron/packages/api/:memory:?cache=shared-wal b/evodron/packages/api/:memory:?cache=shared-wal new file mode 100644 index 00000000..e69de29b diff --git a/evodron/packages/api/package.json b/evodron/packages/api/package.json new file mode 100644 index 00000000..072133bc --- /dev/null +++ b/evodron/packages/api/package.json @@ -0,0 +1,33 @@ +{ + "name": "@evodron/api", + "version": "0.1.0", + "description": "EVODRON REST API", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js", + "test": "vitest run src", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@evodron/db": "*", + "@evodron/shared": "*", + "@fastify/cors": "^10.1.0", + "@fastify/jwt": "^10.2.1", + "@fastify/rate-limit": "^10.3.0", + "bcryptjs": "^2.4.3", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.45.2", + "fastify": "^5.9.0", + "nanoid": "^5.0.4", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/node": "^20.11.25", + "tsx": "^4.7.1", + "typescript": "^5.3.3", + "vitest": "^4.1.10" + } +} diff --git a/evodron/packages/api/src/__tests__/api.test.ts b/evodron/packages/api/src/__tests__/api.test.ts new file mode 100644 index 00000000..6ed1d4a4 --- /dev/null +++ b/evodron/packages/api/src/__tests__/api.test.ts @@ -0,0 +1,476 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { buildApp } from '../index'; +import type { FastifyInstance } from 'fastify'; +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import { getDb, schema } from '@evodron/db'; +import { eq } from 'drizzle-orm'; + +let app: FastifyInstance; + +// Use a unique temp file for each test run to avoid shared state +const testDbPath = path.join(os.tmpdir(), `evodron-test-${Date.now()}.db`); + +// Set up required env before building app +process.env['EVODRON_JWT_SECRET'] = 'test-secret-that-is-long-enough-32chars!!'; +process.env['EVODRON_DB_URL'] = `file:${testDbPath}`; + +beforeAll(async () => { + // Run migrations on the same file that getDb() will use + const Database = (await import('better-sqlite3')).default; + const db = new Database(testDbPath); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + + const migrations = [ + `CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, referral_code TEXT NOT NULL UNIQUE, referred_by TEXT, credits INTEGER NOT NULL DEFAULT 0, level TEXT NOT NULL DEFAULT 'bronze', is_admin INTEGER NOT NULL DEFAULT 0, is_banned INTEGER NOT NULL DEFAULT 0, consent_metrics INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, started_at TEXT NOT NULL, ended_at TEXT, commands_count INTEGER NOT NULL DEFAULT 0, client_version TEXT, ip_hash TEXT)`, + `CREATE TABLE IF NOT EXISTS referrals (id TEXT PRIMARY KEY, referrer_id TEXT NOT NULL, referee_id TEXT NOT NULL UNIQUE, status TEXT NOT NULL DEFAULT 'pending', activated_at TEXT, rewarded_at TEXT, created_at TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS rewards (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, type TEXT NOT NULL, amount INTEGER NOT NULL, source TEXT NOT NULL, created_at TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS reward_rules (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, trigger TEXT NOT NULL, credits INTEGER NOT NULL, active INTEGER NOT NULL DEFAULT 1, description TEXT, updated_at TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS abuse_flags (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, reason TEXT NOT NULL, detail TEXT, resolved_at TEXT, flagged_at TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS ip_registry (id TEXT PRIMARY KEY, ip_hash TEXT NOT NULL, user_id TEXT NOT NULL, seen_at TEXT NOT NULL)`, + `CREATE TABLE IF NOT EXISTS refresh_tokens (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL, revoked_at TEXT)`, + `INSERT OR IGNORE INTO reward_rules (id, name, trigger, credits, active, description, updated_at) VALUES + ('rule-referrer', 'referral_active_referrer', 'referral_active', 100, 1, 'Credits awarded to the referrer when a referee reaches active status', '${new Date().toISOString()}'), + ('rule-onboarding', 'referral_onboarding_referee', 'referral_onboarding', 50, 1, 'Onboarding credits awarded to a referred user', '${new Date().toISOString()}'), + ('rule-milestone', 'milestone_10_sessions', 'milestone_10_sessions', 25, 1, 'Bonus credits for completing 10 sessions', '${new Date().toISOString()}')`, + ]; + + db.transaction(() => { + for (const sql of migrations) db.prepare(sql).run(); + })(); + db.close(); + + app = await buildApp(); + await app.ready(); +}); + +afterAll(async () => { + await app?.close(); + // Clean up test DB file + try { fs.unlinkSync(testDbPath); } catch { /* ignore */ } + try { fs.unlinkSync(testDbPath + '-wal'); } catch { /* ignore */ } + try { fs.unlinkSync(testDbPath + '-shm'); } catch { /* ignore */ } +}); + +describe('Health check', () => { + it('GET /health returns ok', async () => { + const res = await app.inject({ method: 'GET', url: '/health' }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.status).toBe('ok'); + expect(body.service).toBe('evodron-api'); + expect(body.db).toBe('ok'); + expect(body.version).toBeDefined(); + }); +}); + +describe('Auth — register and login', () => { + const email = `test-${Date.now()}@example.com`; + const username = `user${Date.now()}`; + const password = 'StrongPass123!'; + + it('POST /auth/register creates a user', async () => { + const res = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { email, username, password }, + }); + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.success).toBe(true); + expect(body.data.accessToken).toBeDefined(); + expect(body.data.refreshToken).toBeDefined(); + }); + + it('POST /auth/register rejects duplicate email', async () => { + const res = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { email, username: username + '2', password }, + }); + expect(res.statusCode).toBe(409); + }); + + it('POST /auth/login with valid credentials returns tokens', async () => { + const res = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { email, password }, + }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.success).toBe(true); + expect(body.data.accessToken).toBeDefined(); + }); + + it('POST /auth/login with wrong password returns 401', async () => { + const res = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { email, password: 'wrongpassword' }, + }); + expect(res.statusCode).toBe(401); + }); + + it('POST /auth/refresh rotates refresh tokens', async () => { + const loginRes = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { email, password }, + }); + const firstRefreshToken = loginRes.json().data.refreshToken as string; + + const refreshRes = await app.inject({ + method: 'POST', + url: '/auth/refresh', + payload: { refreshToken: firstRefreshToken }, + }); + expect(refreshRes.statusCode).toBe(200); + const rotatedRefreshToken = refreshRes.json().data.refreshToken as string; + expect(rotatedRefreshToken).toBeDefined(); + expect(rotatedRefreshToken).not.toBe(firstRefreshToken); + + const reusedOldTokenRes = await app.inject({ + method: 'POST', + url: '/auth/refresh', + payload: { refreshToken: firstRefreshToken }, + }); + expect(reusedOldTokenRes.statusCode).toBe(401); + }); + + it('POST /auth/logout revokes refresh tokens', async () => { + const loginRes = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { email, password }, + }); + const refreshToken = loginRes.json().data.refreshToken as string; + + const logoutRes = await app.inject({ + method: 'POST', + url: '/auth/logout', + payload: { refreshToken }, + }); + expect(logoutRes.statusCode).toBe(200); + + const refreshRes = await app.inject({ + method: 'POST', + url: '/auth/refresh', + payload: { refreshToken }, + }); + expect(refreshRes.statusCode).toBe(401); + }); +}); + +describe('Users — /users/me', () => { + let accessToken: string; + + beforeAll(async () => { + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `me-${Date.now()}@example.com`, + username: `meuser${Date.now()}`, + password: 'StrongPass123!', + }, + }); + accessToken = reg.json().data.accessToken; + }); + + it('GET /users/me returns profile', async () => { + const res = await app.inject({ + method: 'GET', + url: '/users/me', + headers: { authorization: 'Bearer ' + accessToken }, + }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.success).toBe(true); + expect(body.data.referralCode).toBeDefined(); + expect(body.data.referralLink).toContain('/ref/'); + }); + + it('GET /users/me without token returns 401', async () => { + const res = await app.inject({ method: 'GET', url: '/users/me' }); + expect(res.statusCode).toBe(401); + }); +}); + +describe('Referral system', () => { + let referrerToken: string; + let referralCode: string; + + beforeAll(async () => { + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `referrer-${Date.now()}@example.com`, + username: `referrer${Date.now()}`, + password: 'StrongPass123!', + }, + }); + const data = reg.json().data; + referrerToken = data.accessToken; + referralCode = data.referralCode; + }); + + it('GET /referrals/code returns code and link', async () => { + const res = await app.inject({ + method: 'GET', + url: '/referrals/code', + headers: { authorization: 'Bearer ' + referrerToken }, + }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.data.code).toHaveLength(8); + expect(body.data.link).toContain('/ref/'); + }); + + it('Registering with valid referral code creates pending referral', async () => { + const regRes = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `referee-${Date.now()}@example.com`, + username: `referee${Date.now()}`, + password: 'StrongPass123!', + referralCode, + }, + }); + expect(regRes.statusCode).toBe(201); + + // Check referral stats + const statsRes = await app.inject({ + method: 'GET', + url: '/referrals/stats', + headers: { authorization: 'Bearer ' + referrerToken }, + }); + const stats = statsRes.json().data; + expect(stats.totalReferrals).toBeGreaterThanOrEqual(1); + expect(stats.pendingReferrals).toBeGreaterThanOrEqual(1); + }); + + it('Registering with invalid referral code returns 400', async () => { + const res = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `badref-${Date.now()}@example.com`, + username: `badref${Date.now()}`, + password: 'StrongPass123!', + referralCode: 'INVALID1', + }, + }); + expect(res.statusCode).toBe(400); + expect(res.json().code).toBe('INVALID_REFERRAL'); + }); + + it('Referrer receives credits after referee completes the activation threshold', async () => { + const registerReferee = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `threshold-${Date.now()}@example.com`, + username: `threshold${Date.now()}`, + password: 'StrongPass123!', + referralCode, + }, + }); + const refereeToken = registerReferee.json().data.accessToken as string; + + for (let i = 0; i < 3; i++) { + const startRes = await app.inject({ + method: 'POST', + url: '/sessions/start', + headers: { authorization: 'Bearer ' + refereeToken }, + payload: { clientVersion: '0.1.0' }, + }); + expect(startRes.statusCode).toBe(201); + + const sessionId = startRes.json().data.sessionId as string; + const endRes = await app.inject({ + method: 'POST', + url: '/sessions/end', + headers: { authorization: 'Bearer ' + refereeToken }, + payload: { sessionId, commandsCount: i + 1 }, + }); + expect(endRes.statusCode).toBe(200); + } + + const rewardsRes = await app.inject({ + method: 'GET', + url: '/rewards', + headers: { authorization: 'Bearer ' + referrerToken }, + }); + expect(rewardsRes.statusCode).toBe(200); + expect( + rewardsRes.json().data.rewards.some((reward: { type: string }) => reward.type === 'referral_active'), + ).toBe(true); + }); +}); + +describe('Sessions', () => { + let token: string; + let sessionId: string; + + beforeAll(async () => { + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `session-${Date.now()}@example.com`, + username: `sessionuser${Date.now()}`, + password: 'StrongPass123!', + }, + }); + token = reg.json().data.accessToken; + }); + + it('POST /sessions/start returns sessionId', async () => { + const res = await app.inject({ + method: 'POST', + url: '/sessions/start', + headers: { authorization: 'Bearer ' + token }, + payload: { clientVersion: '0.1.0' }, + }); + expect(res.statusCode).toBe(201); + sessionId = res.json().data.sessionId; + expect(sessionId).toBeDefined(); + }); + + it('POST /sessions/end completes session', async () => { + const res = await app.inject({ + method: 'POST', + url: '/sessions/end', + headers: { authorization: 'Bearer ' + token }, + payload: { sessionId, commandsCount: 5 }, + }); + expect(res.statusCode).toBe(200); + }); +}); + +describe('Rewards', () => { + let token: string; + + beforeAll(async () => { + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `rewards-${Date.now()}@example.com`, + username: `rewarduser${Date.now()}`, + password: 'StrongPass123!', + }, + }); + token = reg.json().data.accessToken; + }); + + it('GET /rewards returns reward history', async () => { + const res = await app.inject({ + method: 'GET', + url: '/rewards', + headers: { authorization: 'Bearer ' + token }, + }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.success).toBe(true); + expect(Array.isArray(body.data.rewards)).toBe(true); + }); +}); + +describe('Stats', () => { + let token: string; + + beforeAll(async () => { + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `stats-${Date.now()}@example.com`, + username: `statsuser${Date.now()}`, + password: 'StrongPass123!', + }, + }); + token = reg.json().data.accessToken; + }); + + it('GET /stats returns usage stats', async () => { + const res = await app.inject({ + method: 'GET', + url: '/stats', + headers: { authorization: 'Bearer ' + token }, + }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.data.level).toBe('bronze'); + expect(body.data.totalSessions).toBe(0); + }); +}); + +describe('Admin access control', () => { + it('rejects non-admin users', async () => { + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email: `admin-block-${Date.now()}@example.com`, + username: `adminblock${Date.now()}`, + password: 'StrongPass123!', + }, + }); + + const token = reg.json().data.accessToken as string; + const res = await app.inject({ + method: 'GET', + url: '/admin/abuse-flags', + headers: { authorization: 'Bearer ' + token }, + }); + + expect(res.statusCode).toBe(403); + }); + + it('allows admin users to inspect abuse flags', async () => { + const email = `admin-${Date.now()}@example.com`; + const reg = await app.inject({ + method: 'POST', + url: '/auth/register', + payload: { + email, + username: `admin${Date.now()}`, + password: 'StrongPass123!', + }, + }); + + const userId = reg.json().data.userId as string; + await getDb() + .update(schema.users) + .set({ isAdmin: true, updatedAt: new Date().toISOString() }) + .where(eq(schema.users.id, userId)); + + const loginRes = await app.inject({ + method: 'POST', + url: '/auth/login', + payload: { + email, + password: 'StrongPass123!', + }, + }); + + expect(loginRes.statusCode).toBe(200); + const accessToken = loginRes.json().data.accessToken as string; + + const res = await app.inject({ + method: 'GET', + url: '/admin/abuse-flags', + headers: { authorization: 'Bearer ' + accessToken }, + }); + + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.json().data.flags)).toBe(true); + }); +}); diff --git a/evodron/packages/api/src/__tests__/unit/helpers.test.ts b/evodron/packages/api/src/__tests__/unit/helpers.test.ts new file mode 100644 index 00000000..26d1361c --- /dev/null +++ b/evodron/packages/api/src/__tests__/unit/helpers.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import { hashIp, randomCode, uuid, now } from '../../middleware/helpers'; + +describe('helpers', () => { + describe('hashIp', () => { + it('returns a 64-char hex string', () => { + const hash = hashIp('192.168.1.1'); + expect(hash).toHaveLength(64); + expect(/^[0-9a-f]+$/.test(hash)).toBe(true); + }); + + it('is deterministic for the same input', () => { + expect(hashIp('10.0.0.1')).toBe(hashIp('10.0.0.1')); + }); + + it('produces different hashes for different IPs', () => { + expect(hashIp('1.1.1.1')).not.toBe(hashIp('2.2.2.2')); + }); + }); + + describe('randomCode', () => { + it('generates a code of the default length (8)', () => { + expect(randomCode()).toHaveLength(8); + }); + + it('generates a code of a custom length', () => { + expect(randomCode(12)).toHaveLength(12); + }); + + it('generates different codes on each call', () => { + const codes = new Set(Array.from({ length: 20 }, () => randomCode())); + expect(codes.size).toBeGreaterThan(15); + }); + + it('only contains safe alphanumeric characters', () => { + const code = randomCode(100); + expect(/^[A-Za-z0-9]+$/.test(code)).toBe(true); + }); + }); + + describe('uuid', () => { + it('returns a valid UUID v4 format', () => { + const id = uuid(); + expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + }); + + it('generates unique values', () => { + const ids = new Set(Array.from({ length: 50 }, () => uuid())); + expect(ids.size).toBe(50); + }); + }); + + describe('now', () => { + it('returns a valid ISO 8601 string', () => { + const ts = now(); + expect(() => new Date(ts).toISOString()).not.toThrow(); + expect(ts).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + }); +}); diff --git a/evodron/packages/api/src/index.ts b/evodron/packages/api/src/index.ts new file mode 100644 index 00000000..eb078e5f --- /dev/null +++ b/evodron/packages/api/src/index.ts @@ -0,0 +1,125 @@ +import 'dotenv/config'; +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import jwt from '@fastify/jwt'; +import rateLimit from '@fastify/rate-limit'; + +import { authRoutes } from './routes/auth'; +import { userRoutes } from './routes/users'; +import { sessionRoutes } from './routes/sessions'; +import { referralRoutes } from './routes/referrals'; +import { rewardRoutes } from './routes/rewards'; +import { statsRoutes } from './routes/stats'; +import { adminRoutes } from './routes/admin'; +import { getDb, schema } from '@evodron/db'; + +const PORT = parseInt(process.env['EVODRON_API_PORT'] ?? '3000', 10); +const HOST = process.env['EVODRON_API_HOST'] ?? '0.0.0.0'; +const BODY_LIMIT = parseInt(process.env['EVODRON_BODY_LIMIT_BYTES'] ?? '1048576', 10); + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env['LOG_LEVEL'] ?? 'info', + }, + trustProxy: true, + bodyLimit: BODY_LIMIT, + }); + + // ── Plugins ────────────────────────────────────────────────────────────── + + const corsOrigin = process.env['EVODRON_CORS_ORIGIN']; + const allowedOrigins = corsOrigin + ? corsOrigin.split(',').map((origin) => origin.trim()).filter(Boolean) + : null; + + await app.register(cors, { + origin: allowedOrigins ?? true, + credentials: true, + }); + + await app.register(rateLimit, { + max: 100, + timeWindow: '1 minute', + errorResponseBuilder: () => ({ + success: false, + error: 'Too many requests. Please slow down.', + code: 'RATE_LIMITED', + }), + }); + + const jwtSecret = process.env['EVODRON_JWT_SECRET']; + if (!jwtSecret || jwtSecret === 'change-me-to-a-long-random-secret') { + throw new Error('EVODRON_JWT_SECRET must be set to a secure value'); + } + + await app.register(jwt, { + secret: jwtSecret, + sign: { expiresIn: '15m' }, + }); + + // ── Database ───────────────────────────────────────────────────────────── + + const db = getDb(); + app.decorate('db', db); + + // ── Routes ─────────────────────────────────────────────────────────────── + + await app.register(authRoutes, { prefix: '/auth' }); + await app.register(userRoutes, { prefix: '/users' }); + await app.register(sessionRoutes, { prefix: '/sessions' }); + await app.register(referralRoutes, { prefix: '/referrals' }); + await app.register(rewardRoutes, { prefix: '/rewards' }); + await app.register(statsRoutes, { prefix: '/stats' }); + await app.register(adminRoutes, { prefix: '/admin' }); + + // ── Health check ───────────────────────────────────────────────────────── + + app.get('/health', async (_request, reply) => { + const status = { + status: 'ok', + service: 'evodron-api', + version: process.env['npm_package_version'] ?? '0.1.0', + db: 'ok', + timestamp: new Date().toISOString(), + }; + + try { + await db.select({ id: schema.users.id }).from(schema.users).limit(1).all(); + } catch { + status.status = 'degraded'; + status.db = 'error'; + } + + return reply.code(status.status === 'ok' ? 200 : 503).send(status); + }); + + app.setNotFoundHandler((_request, reply) => { + return reply.code(404).send({ success: false, error: 'Route not found', code: 'NOT_FOUND' }); + }); + + app.setErrorHandler((error, _request, reply) => { + app.log.error(error); + + if (reply.sent) return; + + if ((error as { validation?: unknown }).validation) { + return reply.code(400).send({ success: false, error: 'Invalid input', code: 'VALIDATION_ERROR' }); + } + + return reply.code(500).send({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }); + }); + + return app; +} + +// Start when run directly +if (require.main === module) { + buildApp() + .then((app) => app.listen({ port: PORT, host: HOST })) + .then(() => console.log(`EVODRON API listening on ${HOST}:${PORT}`)) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/evodron/packages/api/src/middleware/auth.ts b/evodron/packages/api/src/middleware/auth.ts new file mode 100644 index 00000000..b540ad6a --- /dev/null +++ b/evodron/packages/api/src/middleware/auth.ts @@ -0,0 +1,27 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; + +export async function authenticate( + request: FastifyRequest, + reply: FastifyReply, +): Promise { + try { + await request.jwtVerify(); + } catch { + return reply.code(401).send({ success: false, error: 'Unauthorized', code: 'UNAUTHORIZED' }); + } +} + +export async function authenticateAdmin( + request: FastifyRequest, + reply: FastifyReply, +): Promise { + try { + await request.jwtVerify(); + const payload = request.user as { isAdmin?: boolean }; + if (!payload.isAdmin) { + return reply.code(403).send({ success: false, error: 'Forbidden', code: 'FORBIDDEN' }); + } + } catch { + return reply.code(401).send({ success: false, error: 'Unauthorized', code: 'UNAUTHORIZED' }); + } +} diff --git a/evodron/packages/api/src/middleware/helpers.ts b/evodron/packages/api/src/middleware/helpers.ts new file mode 100644 index 00000000..0e806baf --- /dev/null +++ b/evodron/packages/api/src/middleware/helpers.ts @@ -0,0 +1,45 @@ +import crypto from 'crypto'; + +/** + * Hash an IP address so it is never stored in plaintext. + */ +export function hashIp(ip: string): string { + return crypto.createHash('sha256').update(ip).digest('hex'); +} + +/** + * Generate a random alphanumeric string of the given length. + * Uses rejection sampling to avoid modulo bias. + */ +export function randomCode(length = 8): string { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; + const charsLen = chars.length; + // Maximum value that produces unbiased output with rejection sampling + const maxUnbiased = 256 - (256 % charsLen); + let result = ''; + while (result.length < length) { + const bytes = crypto.randomBytes(length * 2); // over-generate to reduce rounds + for (let i = 0; i < bytes.length && result.length < length; i++) { + const byte = bytes[i]!; + // Reject bytes in the biased range + if (byte < maxUnbiased) { + result += chars[byte % charsLen]; + } + } + } + return result; +} + +/** + * Generate a UUIDv4. + */ +export function uuid(): string { + return crypto.randomUUID(); +} + +/** + * Return the current ISO timestamp. + */ +export function now(): string { + return new Date().toISOString(); +} diff --git a/evodron/packages/api/src/routes/admin.ts b/evodron/packages/api/src/routes/admin.ts new file mode 100644 index 00000000..50c02ad7 --- /dev/null +++ b/evodron/packages/api/src/routes/admin.ts @@ -0,0 +1,102 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { eq, and, isNull, desc } from 'drizzle-orm'; +import { getDb, schema } from '@evodron/db'; +import { authenticateAdmin } from '../middleware/auth'; +import { uuid, now } from '../middleware/helpers'; + +export async function adminRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── GET /admin/abuse-flags ──────────────────────────────────────────────── + + app.get( + '/abuse-flags', + { preHandler: [authenticateAdmin] }, + async (_req: FastifyRequest, reply: FastifyReply) => { + const flags = await db + .select() + .from(schema.abuseFlags) + .where(isNull(schema.abuseFlags.resolvedAt)) + .orderBy(desc(schema.abuseFlags.flaggedAt)) + .all(); + + return reply.send({ success: true, data: { flags } }); + }, + ); + + // ── POST /admin/abuse-flags/:id/resolve ─────────────────────────────────── + + app.post<{ Params: { id: string } }>( + '/abuse-flags/:id/resolve', + { preHandler: [authenticateAdmin] }, + async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { + const { id } = req.params; + + await db + .update(schema.abuseFlags) + .set({ resolvedAt: now() }) + .where(eq(schema.abuseFlags.id, id)); + + return reply.send({ success: true, data: { message: 'Flag resolved' } }); + }, + ); + + // ── POST /admin/users/:id/ban ───────────────────────────────────────────── + + app.post<{ Params: { id: string } }>( + '/users/:id/ban', + { preHandler: [authenticateAdmin] }, + async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { + const { id } = req.params; + const timestamp = now(); + + await db + .update(schema.users) + .set({ isBanned: true, updatedAt: timestamp }) + .where(eq(schema.users.id, id)); + + // Flag any pending referrals from this user as fraud + await db + .update(schema.referrals) + .set({ status: 'fraud' }) + .where( + and(eq(schema.referrals.referrerId, id), eq(schema.referrals.status, 'pending')), + ); + + return reply.send({ success: true, data: { message: 'User banned' } }); + }, + ); + + // ── GET /admin/reward-rules ─────────────────────────────────────────────── + + app.get( + '/reward-rules', + { preHandler: [authenticateAdmin] }, + async (_req: FastifyRequest, reply: FastifyReply) => { + const rules = await db.select().from(schema.rewardRules).all(); + return reply.send({ success: true, data: { rules } }); + }, + ); + + // ── PATCH /admin/reward-rules/:id ───────────────────────────────────────── + + app.patch<{ Params: { id: string } }>( + '/reward-rules/:id', + { preHandler: [authenticateAdmin] }, + async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { + const { id } = req.params; + const body = req.body as { credits?: number; active?: boolean; description?: string }; + const timestamp = now(); + + const update: Partial = { updatedAt: timestamp }; + if (typeof body.credits === 'number') update.credits = body.credits; + if (typeof body.active === 'boolean') update.active = body.active; + if (typeof body.description === 'string') update.description = body.description; + + await db.update(schema.rewardRules).set(update).where(eq(schema.rewardRules.id, id)); + + const updated = await db.select().from(schema.rewardRules).where(eq(schema.rewardRules.id, id)).get(); + return reply.send({ success: true, data: { rule: updated } }); + }, + ); +} diff --git a/evodron/packages/api/src/routes/auth.ts b/evodron/packages/api/src/routes/auth.ts new file mode 100644 index 00000000..73c6485e --- /dev/null +++ b/evodron/packages/api/src/routes/auth.ts @@ -0,0 +1,327 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; +import { eq, and, gt, isNull } from 'drizzle-orm'; +import { RegisterSchema, LoginSchema } from '@evodron/shared'; +import { getDb, schema } from '@evodron/db'; +import { hashIp, randomCode, uuid, now } from '../middleware/helpers'; + +const BCRYPT_ROUNDS = 12; +const REFRESH_EXPIRY_DAYS = 30; + +export async function authRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── POST /auth/register ────────────────────────────────────────────────── + + app.post( + '/register', + { + config: { rateLimit: { max: 20, timeWindow: '1 minute' } }, + }, + async (req: FastifyRequest, reply: FastifyReply) => { + const parsed = RegisterSchema.safeParse(req.body); + if (!parsed.success) { + return reply.code(400).send({ + success: false, + error: 'Invalid input', + code: 'VALIDATION_ERROR', + details: parsed.error.flatten(), + }); + } + + const { email, username, password, referralCode } = parsed.data; + + // Check uniqueness + const existing = await db + .select({ id: schema.users.id }) + .from(schema.users) + .where(eq(schema.users.email, email.toLowerCase())) + .get(); + + if (existing) { + return reply.code(409).send({ + success: false, + error: 'Email already registered', + code: 'EMAIL_EXISTS', + }); + } + + const existingUsername = await db + .select({ id: schema.users.id }) + .from(schema.users) + .where(eq(schema.users.username, username)) + .get(); + + if (existingUsername) { + return reply.code(409).send({ + success: false, + error: 'Username already taken', + code: 'USERNAME_EXISTS', + }); + } + + // Resolve referrer + let referrerId: string | undefined; + if (referralCode) { + const referrer = await db + .select({ id: schema.users.id }) + .from(schema.users) + .where(eq(schema.users.referralCode, referralCode)) + .get(); + + if (!referrer) { + return reply.code(400).send({ + success: false, + error: 'Invalid referral code', + code: 'INVALID_REFERRAL', + }); + } + referrerId = referrer.id; + } + + // Hash password + const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS); + const userId = uuid(); + const userReferralCode = randomCode(8); + const timestamp = now(); + + // Create user + await db.insert(schema.users).values({ + id: userId, + email: email.toLowerCase(), + username, + passwordHash, + referralCode: userReferralCode, + referredBy: referrerId ?? null, + createdAt: timestamp, + updatedAt: timestamp, + }); + + // Create referral record + if (referrerId) { + // Anti-self-referral (cannot refer yourself) + if (referrerId === userId) { + // This cannot happen here since user doesn't exist yet, but guard anyway + } else { + await db.insert(schema.referrals).values({ + id: uuid(), + referrerId, + refereeId: userId, + status: 'pending', + createdAt: timestamp, + }); + + // Onboarding reward for referee + const onboardingRule = await db + .select() + .from(schema.rewardRules) + .where( + and( + eq(schema.rewardRules.trigger, 'referral_onboarding'), + eq(schema.rewardRules.active, true), + ), + ) + .get(); + + if (onboardingRule) { + await db.insert(schema.rewards).values({ + id: uuid(), + userId, + type: 'referral_onboarding', + amount: onboardingRule.credits, + source: `Referral onboarding from code ${referralCode}`, + createdAt: timestamp, + }); + + await db + .update(schema.users) + .set({ credits: onboardingRule.credits, updatedAt: timestamp }) + .where(eq(schema.users.id, userId)); + } + } + } + + // Track IP + const ip = (req.headers['x-forwarded-for'] as string) || req.ip || '0.0.0.0'; + const ipHash = hashIp(ip.split(',')[0]!.trim()); + + await db.insert(schema.ipRegistry).values({ + id: uuid(), + ipHash, + userId, + seenAt: timestamp, + }); + + // Detect suspicious IP reuse (same IP already used for multiple accounts) + const ipCount = ( + await db + .select({ userId: schema.ipRegistry.userId }) + .from(schema.ipRegistry) + .where(eq(schema.ipRegistry.ipHash, ipHash)) + ).length; + + if (ipCount > 5) { + await db.insert(schema.abuseFlags).values({ + id: uuid(), + userId, + reason: 'ip_reuse', + detail: `IP hash ${ipHash} has been seen for ${ipCount} accounts`, + flaggedAt: timestamp, + }); + } + + // Issue tokens + const tokens = await issueTokens(app, db, userId, false); + + return reply.code(201).send({ + success: true, + data: { ...tokens, userId, username, referralCode: userReferralCode }, + }); + }, + ); + + // ── POST /auth/login ───────────────────────────────────────────────────── + + app.post( + '/login', + { + config: { rateLimit: { max: 20, timeWindow: '1 minute' } }, + }, + async (req: FastifyRequest, reply: FastifyReply) => { + const parsed = LoginSchema.safeParse(req.body); + if (!parsed.success) { + return reply.code(400).send({ success: false, error: 'Invalid input', code: 'VALIDATION_ERROR' }); + } + + const { email, password } = parsed.data; + + const user = await db + .select() + .from(schema.users) + .where(eq(schema.users.email, email.toLowerCase())) + .get(); + + if (!user) { + // Constant-time response to prevent user enumeration + await bcrypt.hash('dummy', 1); + return reply.code(401).send({ success: false, error: 'Invalid credentials', code: 'INVALID_CREDENTIALS' }); + } + + if (user.isBanned) { + return reply.code(403).send({ success: false, error: 'Account suspended', code: 'ACCOUNT_BANNED' }); + } + + const valid = await bcrypt.compare(password, user.passwordHash); + if (!valid) { + return reply.code(401).send({ success: false, error: 'Invalid credentials', code: 'INVALID_CREDENTIALS' }); + } + + const tokens = await issueTokens(app, db, user.id, user.isAdmin); + + return reply.send({ + success: true, + data: { ...tokens, userId: user.id, username: user.username }, + }); + }, + ); + + // ── POST /auth/refresh ─────────────────────────────────────────────────── + + app.post('/refresh', async (req: FastifyRequest, reply: FastifyReply) => { + const body = req.body as { refreshToken?: string }; + const refreshToken = body?.refreshToken; + + if (!refreshToken) { + return reply.code(400).send({ success: false, error: 'refreshToken required', code: 'MISSING_TOKEN' }); + } + + const tokenHash = hashToken(refreshToken); + const expiry = new Date().toISOString(); + + const stored = await db + .select() + .from(schema.refreshTokens) + .where( + and( + eq(schema.refreshTokens.tokenHash, tokenHash), + gt(schema.refreshTokens.expiresAt, expiry), + isNull(schema.refreshTokens.revokedAt), + ), + ) + .get(); + + if (!stored) { + return reply.code(401).send({ success: false, error: 'Invalid or expired refresh token', code: 'INVALID_TOKEN' }); + } + + const user = await db + .select({ id: schema.users.id, isAdmin: schema.users.isAdmin, isBanned: schema.users.isBanned }) + .from(schema.users) + .where(eq(schema.users.id, stored.userId)) + .get(); + + if (!user || user.isBanned) { + return reply.code(403).send({ success: false, error: 'Account suspended', code: 'ACCOUNT_BANNED' }); + } + + // Revoke old token + await db + .update(schema.refreshTokens) + .set({ revokedAt: now() }) + .where(eq(schema.refreshTokens.id, stored.id)); + + const tokens = await issueTokens(app, db, user.id, user.isAdmin); + return reply.send({ success: true, data: tokens }); + }); + + // ── POST /auth/logout ──────────────────────────────────────────────────── + + app.post('/logout', async (req: FastifyRequest, reply: FastifyReply) => { + const body = req.body as { refreshToken?: string }; + const refreshToken = body?.refreshToken; + + if (refreshToken) { + const tokenHash = hashToken(refreshToken); + await db + .update(schema.refreshTokens) + .set({ revokedAt: now() }) + .where(eq(schema.refreshTokens.tokenHash, tokenHash)); + } + + return reply.send({ success: true, data: { message: 'Logged out' } }); + }); +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function hashToken(token: string): string { + return crypto.createHash('sha256').update(token).digest('hex'); +} + +async function issueTokens( + app: FastifyInstance, + db: ReturnType, + userId: string, + isAdmin: boolean, +) { + const accessToken = app.jwt.sign({ sub: userId, isAdmin }, { expiresIn: '15m' }); + + const rawRefresh = crypto.randomBytes(48).toString('hex'); + const tokenHash = hashToken(rawRefresh); + const expiresAt = new Date(Date.now() + REFRESH_EXPIRY_DAYS * 86400000).toISOString(); + + await db.insert(schema.refreshTokens).values({ + id: uuid(), + userId, + tokenHash, + expiresAt, + createdAt: now(), + }); + + return { + accessToken, + refreshToken: rawRefresh, + expiresIn: 900, + }; +} diff --git a/evodron/packages/api/src/routes/referrals.ts b/evodron/packages/api/src/routes/referrals.ts new file mode 100644 index 00000000..5743ebd9 --- /dev/null +++ b/evodron/packages/api/src/routes/referrals.ts @@ -0,0 +1,109 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { eq, and } from 'drizzle-orm'; +import { getDb, schema } from '@evodron/db'; +import { authenticate } from '../middleware/auth'; + +export async function referralRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── GET /referrals/code ────────────────────────────────────────────────── + + app.get( + '/code', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + + const user = await db + .select({ referralCode: schema.users.referralCode }) + .from(schema.users) + .where(eq(schema.users.id, userId)) + .get(); + + if (!user) { + return reply.code(404).send({ success: false, error: 'User not found', code: 'NOT_FOUND' }); + } + + const base = process.env['EVODRON_REFERRAL_BASE_URL'] ?? 'https://evodron.io'; + const link = `${base}/ref/${user.referralCode}`; + + return reply.send({ success: true, data: { code: user.referralCode, link } }); + }, + ); + + // ── GET /referrals/stats ───────────────────────────────────────────────── + + app.get( + '/stats', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + + const user = await db + .select({ referralCode: schema.users.referralCode }) + .from(schema.users) + .where(eq(schema.users.id, userId)) + .get(); + + if (!user) { + return reply.code(404).send({ success: false, error: 'User not found', code: 'NOT_FOUND' }); + } + + const allReferrals = await db + .select({ + id: schema.referrals.id, + refereeId: schema.referrals.refereeId, + status: schema.referrals.status, + createdAt: schema.referrals.createdAt, + }) + .from(schema.referrals) + .where(eq(schema.referrals.referrerId, userId)) + .all(); + + // Fetch referee usernames + const referralEntries = await Promise.all( + allReferrals.map(async (r) => { + const referee = await db + .select({ username: schema.users.username }) + .from(schema.users) + .where(eq(schema.users.id, r.refereeId)) + .get(); + return { + id: r.id, + refereeUsername: referee?.username ?? '(deleted)', + status: r.status, + createdAt: r.createdAt, + }; + }), + ); + + const totalCreditsEarned = ( + await db + .select({ amount: schema.rewards.amount }) + .from(schema.rewards) + .where( + and( + eq(schema.rewards.userId, userId), + eq(schema.rewards.type, 'referral_active'), + ), + ) + .all() + ).reduce((sum, r) => sum + r.amount, 0); + + const base = process.env['EVODRON_REFERRAL_BASE_URL'] ?? 'https://evodron.io'; + + return reply.send({ + success: true, + data: { + code: user.referralCode, + link: `${base}/ref/${user.referralCode}`, + totalReferrals: allReferrals.length, + activeReferrals: allReferrals.filter((r) => r.status === 'active' || r.status === 'rewarded').length, + pendingReferrals: allReferrals.filter((r) => r.status === 'pending').length, + totalCreditsEarned, + referrals: referralEntries, + }, + }); + }, + ); +} diff --git a/evodron/packages/api/src/routes/rewards.ts b/evodron/packages/api/src/routes/rewards.ts new file mode 100644 index 00000000..0267ed98 --- /dev/null +++ b/evodron/packages/api/src/routes/rewards.ts @@ -0,0 +1,58 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { eq, desc } from 'drizzle-orm'; +import { getDb, schema } from '@evodron/db'; +import { authenticate } from '../middleware/auth'; + +export async function rewardRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── GET /rewards ───────────────────────────────────────────────────────── + + app.get( + '/', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + + const rewardList = await db + .select() + .from(schema.rewards) + .where(eq(schema.rewards.userId, userId)) + .orderBy(desc(schema.rewards.createdAt)) + .all(); + + const total = rewardList.reduce((sum, r) => sum + r.amount, 0); + + return reply.send({ + success: true, + data: { + total, + rewards: rewardList, + }, + }); + }, + ); + + // ── GET /rewards/rules ──────────────────────────────────────────────────── + + app.get( + '/rules', + { preHandler: [authenticate] }, + async (_req: FastifyRequest, reply: FastifyReply) => { + const rules = await db + .select({ + id: schema.rewardRules.id, + name: schema.rewardRules.name, + trigger: schema.rewardRules.trigger, + credits: schema.rewardRules.credits, + active: schema.rewardRules.active, + description: schema.rewardRules.description, + }) + .from(schema.rewardRules) + .where(eq(schema.rewardRules.active, true)) + .all(); + + return reply.send({ success: true, data: { rules } }); + }, + ); +} diff --git a/evodron/packages/api/src/routes/sessions.ts b/evodron/packages/api/src/routes/sessions.ts new file mode 100644 index 00000000..6b5aebed --- /dev/null +++ b/evodron/packages/api/src/routes/sessions.ts @@ -0,0 +1,243 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { eq, and, isNotNull } from 'drizzle-orm'; +import { getDb, schema } from '@evodron/db'; +import { authenticate } from '../middleware/auth'; +import { uuid, now, hashIp } from '../middleware/helpers'; +import { StartSessionSchema, EndSessionSchema } from '@evodron/shared'; + +export async function sessionRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── POST /sessions/start ───────────────────────────────────────────────── + + app.post( + '/start', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + const parsed = StartSessionSchema.safeParse(req.body); + if (!parsed.success) { + return reply.code(400).send({ success: false, error: 'Invalid input', code: 'VALIDATION_ERROR' }); + } + + const ip = (req.headers['x-forwarded-for'] as string) || req.ip || '0.0.0.0'; + const ipHash = hashIp(ip.split(',')[0]!.trim()); + const sessionId = uuid(); + const timestamp = now(); + + await db.insert(schema.sessions).values({ + id: sessionId, + userId, + startedAt: timestamp, + clientVersion: parsed.data.clientVersion ?? null, + ipHash, + }); + + // Record IP for abuse detection + await db.insert(schema.ipRegistry).values({ + id: uuid(), + ipHash, + userId, + seenAt: timestamp, + }); + + return reply.code(201).send({ success: true, data: { sessionId } }); + }, + ); + + // ── POST /sessions/end ─────────────────────────────────────────────────── + + app.post( + '/end', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + const parsed = EndSessionSchema.safeParse(req.body); + if (!parsed.success) { + return reply.code(400).send({ success: false, error: 'Invalid input', code: 'VALIDATION_ERROR' }); + } + + const { sessionId, commandsCount } = parsed.data; + const timestamp = now(); + + const session = await db + .select() + .from(schema.sessions) + .where(and(eq(schema.sessions.id, sessionId), eq(schema.sessions.userId, userId))) + .get(); + + if (!session) { + return reply.code(404).send({ success: false, error: 'Session not found', code: 'NOT_FOUND' }); + } + + await db + .update(schema.sessions) + .set({ endedAt: timestamp, commandsCount }) + .where(eq(schema.sessions.id, sessionId)); + + // Check if a referral should be activated + await checkAndActivateReferral(db, userId, timestamp); + + // Check milestones + await checkMilestones(db, userId, timestamp); + + return reply.send({ success: true, data: { sessionId } }); + }, + ); +} + +// ─── Referral activation ────────────────────────────────────────────────────── + +async function checkAndActivateReferral( + db: ReturnType, + userId: string, + timestamp: string, +) { + const minSessions = parseInt(process.env['EVODRON_REWARD_MIN_SESSIONS'] ?? '3', 10); + + const completedSessions = await db + .select({ id: schema.sessions.id }) + .from(schema.sessions) + .where(and(eq(schema.sessions.userId, userId), isNotNull(schema.sessions.endedAt))) + .all(); + + if (completedSessions.length < minSessions) return; + + // Look for pending referral where this user is the referee + const referral = await db + .select() + .from(schema.referrals) + .where( + and(eq(schema.referrals.refereeId, userId), eq(schema.referrals.status, 'pending')), + ) + .get(); + + if (!referral) return; + + // Activate referral + await db + .update(schema.referrals) + .set({ status: 'active', activatedAt: timestamp }) + .where(eq(schema.referrals.id, referral.id)); + + // Get reward rule for referrer + const referrerRule = await db + .select() + .from(schema.rewardRules) + .where( + and( + eq(schema.rewardRules.trigger, 'referral_active'), + eq(schema.rewardRules.active, true), + ), + ) + .get(); + + if (referrerRule) { + // Credit referrer + await db.insert(schema.rewards).values({ + id: uuid(), + userId: referral.referrerId, + type: 'referral_active', + amount: referrerRule.credits, + source: `Referral activated: ${userId}`, + createdAt: timestamp, + }); + + const referrer = await db + .select({ credits: schema.users.credits }) + .from(schema.users) + .where(eq(schema.users.id, referral.referrerId)) + .get(); + + if (referrer) { + await db + .update(schema.users) + .set({ + credits: referrer.credits + referrerRule.credits, + updatedAt: timestamp, + }) + .where(eq(schema.users.id, referral.referrerId)); + } + + // Mark referral as rewarded + await db + .update(schema.referrals) + .set({ status: 'rewarded', rewardedAt: timestamp }) + .where(eq(schema.referrals.id, referral.id)); + } +} + +// ─── Milestone check ────────────────────────────────────────────────────────── + +async function checkMilestones( + db: ReturnType, + userId: string, + timestamp: string, +) { + const completedCount = ( + await db + .select({ id: schema.sessions.id }) + .from(schema.sessions) + .where(and(eq(schema.sessions.userId, userId), isNotNull(schema.sessions.endedAt))) + .all() + ).length; + + const milestoneTrigger = `milestone_${completedCount}_sessions`; + + const rule = await db + .select() + .from(schema.rewardRules) + .where( + and( + eq(schema.rewardRules.trigger, milestoneTrigger), + eq(schema.rewardRules.active, true), + ), + ) + .get(); + + if (!rule) return; + + // Only award once — check if already awarded + const alreadyAwarded = await db + .select({ id: schema.rewards.id }) + .from(schema.rewards) + .where( + and( + eq(schema.rewards.userId, userId), + eq(schema.rewards.source, milestoneTrigger), + ), + ) + .get(); + + if (alreadyAwarded) return; + + await db.insert(schema.rewards).values({ + id: uuid(), + userId, + type: 'milestone', + amount: rule.credits, + source: milestoneTrigger, + createdAt: timestamp, + }); + + const user = await db + .select({ credits: schema.users.credits }) + .from(schema.users) + .where(eq(schema.users.id, userId)) + .get(); + + if (user) { + const newCredits = user.credits + rule.credits; + const newLevel = computeLevel(newCredits); + await db + .update(schema.users) + .set({ credits: newCredits, level: newLevel, updatedAt: timestamp }) + .where(eq(schema.users.id, userId)); + } +} + +function computeLevel(credits: number): 'bronze' | 'silver' | 'gold' { + if (credits >= 1000) return 'gold'; + if (credits >= 300) return 'silver'; + return 'bronze'; +} diff --git a/evodron/packages/api/src/routes/stats.ts b/evodron/packages/api/src/routes/stats.ts new file mode 100644 index 00000000..84aa0fc6 --- /dev/null +++ b/evodron/packages/api/src/routes/stats.ts @@ -0,0 +1,52 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { eq } from 'drizzle-orm'; +import { getDb, schema } from '@evodron/db'; +import { authenticate } from '../middleware/auth'; + +export async function statsRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── GET /stats ──────────────────────────────────────────────────────────── + + app.get( + '/', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + + const user = await db + .select({ + credits: schema.users.credits, + level: schema.users.level, + createdAt: schema.users.createdAt, + }) + .from(schema.users) + .where(eq(schema.users.id, userId)) + .get(); + + if (!user) { + return reply.code(404).send({ success: false, error: 'User not found', code: 'NOT_FOUND' }); + } + + const sessionList = await db + .select({ commandsCount: schema.sessions.commandsCount }) + .from(schema.sessions) + .where(eq(schema.sessions.userId, userId)) + .all(); + + const totalSessions = sessionList.length; + const totalCommands = sessionList.reduce((s, r) => s + r.commandsCount, 0); + + return reply.send({ + success: true, + data: { + totalSessions, + totalCommands, + totalCredits: user.credits, + level: user.level, + joinedAt: user.createdAt, + }, + }); + }, + ); +} diff --git a/evodron/packages/api/src/routes/users.ts b/evodron/packages/api/src/routes/users.ts new file mode 100644 index 00000000..fbd54565 --- /dev/null +++ b/evodron/packages/api/src/routes/users.ts @@ -0,0 +1,70 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { eq } from 'drizzle-orm'; +import { getDb, schema } from '@evodron/db'; +import { authenticate } from '../middleware/auth'; + +export async function userRoutes(app: FastifyInstance) { + const db = getDb(); + + // ── GET /users/me ──────────────────────────────────────────────────────── + + app.get( + '/me', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + + const user = await db + .select({ + id: schema.users.id, + email: schema.users.email, + username: schema.users.username, + referralCode: schema.users.referralCode, + credits: schema.users.credits, + level: schema.users.level, + createdAt: schema.users.createdAt, + }) + .from(schema.users) + .where(eq(schema.users.id, userId)) + .get(); + + if (!user) { + return reply.code(404).send({ success: false, error: 'User not found', code: 'NOT_FOUND' }); + } + + const referralLink = buildReferralLink(user.referralCode); + + return reply.send({ + success: true, + data: { ...user, referralLink }, + }); + }, + ); + + // ── PATCH /users/me/consent ────────────────────────────────────────────── + + app.patch( + '/me/consent', + { preHandler: [authenticate] }, + async (req: FastifyRequest, reply: FastifyReply) => { + const { sub: userId } = req.user as { sub: string }; + const body = req.body as { consentMetrics?: boolean }; + + if (typeof body?.consentMetrics !== 'boolean') { + return reply.code(400).send({ success: false, error: 'consentMetrics (boolean) required', code: 'VALIDATION_ERROR' }); + } + + await db + .update(schema.users) + .set({ consentMetrics: body.consentMetrics, updatedAt: new Date().toISOString() }) + .where(eq(schema.users.id, userId)); + + return reply.send({ success: true, data: { consentMetrics: body.consentMetrics } }); + }, + ); +} + +function buildReferralLink(code: string): string { + const base = process.env['EVODRON_REFERRAL_BASE_URL'] ?? 'https://evodron.io'; + return `${base}/ref/${code}`; +} diff --git a/evodron/packages/api/tsconfig.json b/evodron/packages/api/tsconfig.json new file mode 100644 index 00000000..b224f046 --- /dev/null +++ b/evodron/packages/api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "node16", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "sourceMap": true, + "types": ["node"], + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/evodron/packages/api/vitest.config.mts b/evodron/packages/api/vitest.config.mts new file mode 100644 index 00000000..3d27b8ad --- /dev/null +++ b/evodron/packages/api/vitest.config.mts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@evodron/shared': import.meta.dirname + '/../shared/src/index.ts', + '@evodron/db': import.meta.dirname + '/../db/src/index.ts', + }, + }, + test: { + environment: 'node', + globals: false, + testTimeout: 30000, + }, +}); diff --git a/evodron/packages/cli/package.json b/evodron/packages/cli/package.json new file mode 100644 index 00000000..ca5bf145 --- /dev/null +++ b/evodron/packages/cli/package.json @@ -0,0 +1,29 @@ +{ + "name": "@evodron/cli", + "version": "0.1.0", + "description": "EVODRON CLI — the evodron command", + "bin": { + "evodron": "./dist/bin/evodron.js" + }, + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/bin/evodron.ts", + "start": "node dist/bin/evodron.js", + "test": "vitest run src", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@evodron/shared": "*", + "chalk": "^4.1.2", + "commander": "^12.0.0", + "dotenv": "^16.4.5", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^20.11.25", + "tsx": "^4.7.1", + "typescript": "^5.3.3", + "vitest": "^4.1.10" + } +} diff --git a/evodron/packages/cli/src/bin/evodron.ts b/evodron/packages/cli/src/bin/evodron.ts new file mode 100644 index 00000000..44cd7feb --- /dev/null +++ b/evodron/packages/cli/src/bin/evodron.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import 'dotenv/config'; +import { program } from 'commander'; +import { splash } from '../ui/splash'; +import { registerAuthCommands } from '../commands/auth'; +import { registerReferralCommands } from '../commands/referral'; +import { registerRewardCommands } from '../commands/rewards'; +import { registerStatsCommands } from '../commands/stats'; +import { registerRunCommand } from '../commands/run'; + +splash(); + +program + .name('evodron') + .description('EVODRON — AI-powered developer platform\n\nNote: AI coding features are powered by the GitHub Copilot CLI (third-party component, not affiliated with EVODRON).') + .version('0.1.0'); + +registerAuthCommands(program); +registerReferralCommands(program); +registerRewardCommands(program); +registerStatsCommands(program); +registerRunCommand(program); + +program.parseAsync(process.argv).catch((err: unknown) => { + console.error('Error:', err instanceof Error ? err.message : String(err)); + process.exit(1); +}); diff --git a/evodron/packages/cli/src/commands/auth.ts b/evodron/packages/cli/src/commands/auth.ts new file mode 100644 index 00000000..77c61b7d --- /dev/null +++ b/evodron/packages/cli/src/commands/auth.ts @@ -0,0 +1,153 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import * as readline from 'readline'; +import { apiRequest } from '../services/api'; +import { loadConfig, saveConfig, clearConfig } from '../services/config'; +import type { ApiResponse, AuthTokens } from '@evodron/shared'; + +export function registerAuthCommands(program: Command): void { + const auth = program.command('auth').description('Manage your EVODRON account'); + + // ── evodron auth register ──────────────────────────────────────────────── + + auth + .command('register') + .description('Create a new EVODRON account') + .option('--ref ', 'Referral code from a friend') + .action(async (opts: { ref?: string }) => { + console.log(chalk.bold('\n✨ Create your EVODRON account\n')); + + const email = await prompt('Email: '); + const username = await prompt('Username: '); + const password = await promptPassword('Password (min 8 chars): '); + + const body: Record = { email, username, password }; + if (opts.ref) body['referralCode'] = opts.ref; + + const res = await apiRequest>( + '/auth/register', + { method: 'POST', body: JSON.stringify(body) }, + ); + + if (!res.success) { + console.error(chalk.red(`\n❌ Registration failed: ${res.error}`)); + process.exit(1); + } + + saveConfig({ + accessToken: res.data.accessToken, + refreshToken: res.data.refreshToken, + userId: res.data.userId, + username: res.data.username, + }); + + console.log(chalk.green(`\n✅ Welcome to EVODRON, ${res.data.username}!`)); + if (opts.ref) { + console.log(chalk.yellow('🎁 You received onboarding credits for joining via referral!')); + } + console.log(chalk.dim(`\nYour referral code: ${chalk.bold(res.data.referralCode)}`)); + }); + + // ── evodron auth login ─────────────────────────────────────────────────── + + auth + .command('login') + .description('Log in to your EVODRON account') + .action(async () => { + console.log(chalk.bold('\n🔑 Log in to EVODRON\n')); + const email = await prompt('Email: '); + const password = await promptPassword('Password: '); + + const res = await apiRequest>( + '/auth/login', + { method: 'POST', body: JSON.stringify({ email, password }) }, + ); + + if (!res.success) { + console.error(chalk.red(`\n❌ Login failed: ${res.error}`)); + process.exit(1); + } + + saveConfig({ + accessToken: res.data.accessToken, + refreshToken: res.data.refreshToken, + userId: res.data.userId, + username: res.data.username, + }); + + console.log(chalk.green(`\n✅ Logged in as ${res.data.username}`)); + }); + + // ── evodron auth logout ────────────────────────────────────────────────── + + auth + .command('logout') + .description('Log out of EVODRON') + .action(async () => { + const config = loadConfig(); + if (config.refreshToken) { + await apiRequest('/auth/logout', { + method: 'POST', + body: JSON.stringify({ refreshToken: config.refreshToken }), + }).catch(() => { + // best effort + }); + } + clearConfig(); + console.log(chalk.green('\n✅ Logged out')); + }); + + // ── evodron auth status ────────────────────────────────────────────────── + + auth + .command('status') + .description('Show current login status') + .action(() => { + const config = loadConfig(); + if (config.username && config.accessToken) { + console.log(chalk.green(`\n✅ Logged in as ${chalk.bold(config.username)}`)); + } else { + console.log(chalk.yellow('\n⚠️ Not logged in. Run: evodron auth login')); + } + }); +} + +// ─── Prompt helpers ─────────────────────────────────────────────────────────── + +function prompt(question: string): Promise { + return new Promise((resolve) => { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +function promptPassword(question: string): Promise { + return new Promise((resolve) => { + process.stdout.write(question); + let password = ''; + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + const onData = (ch: string) => { + if (ch === '\r' || ch === '\n') { + process.stdin.setRawMode?.(false); + process.stdin.pause(); + process.stdin.removeListener('data', onData); + process.stdout.write('\n'); + resolve(password); + } else if (ch === '\u0003') { + process.exit(); + } else if (ch === '\u007f') { + password = password.slice(0, -1); + } else { + password += ch; + } + }; + + process.stdin.on('data', onData); + }); +} diff --git a/evodron/packages/cli/src/commands/referral.ts b/evodron/packages/cli/src/commands/referral.ts new file mode 100644 index 00000000..b443da6c --- /dev/null +++ b/evodron/packages/cli/src/commands/referral.ts @@ -0,0 +1,70 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import { apiRequest } from '../services/api'; +import type { ApiResponse, ReferralStats } from '@evodron/shared'; + +export function registerReferralCommands(program: Command): void { + const referral = program + .command('referral') + .description('Manage your EVODRON referrals'); + + // ── evodron referral code ──────────────────────────────────────────────── + + referral + .command('code') + .description('Show your referral code and link') + .action(async () => { + const res = await apiRequest>( + '/referrals/code', + { auth: true }, + ); + + if (!res.success) { + console.error(chalk.red(`❌ ${res.error}`)); + process.exit(1); + } + + console.log(chalk.bold('\n🔗 Your EVODRON Referral\n')); + console.log(` Code : ${chalk.cyan(res.data.code)}`); + console.log(` Link : ${chalk.cyan(res.data.link)}`); + console.log(chalk.dim('\n Share your link and earn credits when friends become active users!\n')); + }); + + // ── evodron referral stats ──────────────────────────────────────────────── + + referral + .command('stats') + .description('Show your referral statistics') + .action(async () => { + const res = await apiRequest>('/referrals/stats', { auth: true }); + + if (!res.success) { + console.error(chalk.red(`❌ ${res.error}`)); + process.exit(1); + } + + const d = res.data; + + console.log(chalk.bold('\n📊 Referral Dashboard\n')); + console.log(` Code : ${chalk.cyan(d.code)}`); + console.log(` Link : ${chalk.cyan(d.link)}`); + console.log(` Total referrals: ${chalk.white(d.totalReferrals)}`); + console.log(` Active : ${chalk.green(d.activeReferrals)}`); + console.log(` Pending : ${chalk.yellow(d.pendingReferrals)}`); + console.log(` Credits earned : ${chalk.yellow(d.totalCreditsEarned + ' ⚡')}`); + + if (d.referrals.length > 0) { + console.log(chalk.bold('\n Referred users:\n')); + for (const r of d.referrals) { + const status = + r.status === 'active' || r.status === 'rewarded' + ? chalk.green(r.status) + : r.status === 'fraud' + ? chalk.red(r.status) + : chalk.yellow(r.status); + console.log(` • ${chalk.white(r.refereeUsername)} — ${status}`); + } + } + console.log(); + }); +} diff --git a/evodron/packages/cli/src/commands/rewards.ts b/evodron/packages/cli/src/commands/rewards.ts new file mode 100644 index 00000000..da7120b1 --- /dev/null +++ b/evodron/packages/cli/src/commands/rewards.ts @@ -0,0 +1,67 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import { apiRequest } from '../services/api'; +import type { ApiResponse, Reward } from '@evodron/shared'; + +export function registerRewardCommands(program: Command): void { + const rewards = program + .command('rewards') + .description('View your EVODRON credits and reward history'); + + rewards + .command('history') + .alias('ls') + .description('Show your reward history') + .action(async () => { + const res = await apiRequest>( + '/rewards', + { auth: true }, + ); + + if (!res.success) { + console.error(chalk.red(`❌ ${res.error}`)); + process.exit(1); + } + + console.log(chalk.bold('\n⚡ EVODRON Credits\n')); + console.log(` Total balance : ${chalk.yellow(res.data.total + ' credits')}`); + + if (res.data.rewards.length === 0) { + console.log(chalk.dim('\n No rewards yet. Refer friends to earn credits!\n')); + return; + } + + console.log(chalk.bold('\n History:\n')); + for (const r of res.data.rewards) { + const date = new Date(r.createdAt).toLocaleDateString(); + console.log( + ` ${chalk.dim(date)} ${chalk.green('+' + r.amount + ' ⚡')} ${chalk.dim(r.source)}`, + ); + } + console.log(); + }); + + rewards + .command('rules') + .description('Show active reward rules') + .action(async () => { + const res = await apiRequest }>>( + '/rewards/rules', + { auth: true }, + ); + + if (!res.success) { + console.error(chalk.red(`❌ ${res.error}`)); + process.exit(1); + } + + console.log(chalk.bold('\n📋 Active Reward Rules\n')); + for (const rule of res.data.rules) { + console.log(` • ${chalk.cyan(rule.name)} — ${chalk.yellow(rule.credits + ' credits')}`); + if (rule.description) { + console.log(` ${chalk.dim(rule.description)}`); + } + } + console.log(chalk.dim('\n ⚠️ Credits are virtual and have no monetary value.\n')); + }); +} diff --git a/evodron/packages/cli/src/commands/run.ts b/evodron/packages/cli/src/commands/run.ts new file mode 100644 index 00000000..e4832398 --- /dev/null +++ b/evodron/packages/cli/src/commands/run.ts @@ -0,0 +1,115 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import { spawn } from 'child_process'; +import { loadConfig } from '../services/config'; +import { apiRequest } from '../services/api'; +import type { ApiResponse } from '@evodron/shared'; + +/** + * `evodron run` — delegates to the GitHub Copilot CLI binary. + * + * The GitHub Copilot CLI is a third-party component, © GitHub, Inc. + * EVODRON is not affiliated with GitHub or Microsoft. + * + * If `ghcs` (GitHub Copilot CLI) is not installed, the command prints + * installation instructions and exits. + */ +export function registerRunCommand(program: Command): void { + program + .command('run [args...]') + .description( + 'Run the GitHub Copilot CLI AI assistant (third-party component)\n' + + ' All arguments are passed directly to the GitHub Copilot CLI.\n' + + ' Example: evodron run -- "explain this code"', + ) + .allowUnknownOption() + .action(async (_args: string[], _opts: unknown, cmd: Command) => { + const config = loadConfig(); + if (!config.accessToken) { + console.error(chalk.yellow('\n⚠️ Please log in first: evodron auth login\n')); + process.exit(1); + } + + // Record session start + let sessionId: string | undefined; + try { + if (config.accessToken) { + const startRes = await apiRequest>('/sessions/start', { + method: 'POST', + auth: true, + body: JSON.stringify({ clientVersion: '0.1.0' }), + }); + if (startRes.success) { + sessionId = startRes.data.sessionId; + } + } + } catch { + // Non-fatal: continue even if session tracking fails + } + + const rawArgs = process.argv.slice(process.argv.indexOf('run') + 1).filter((a) => a !== '--'); + const copilotBin = findCopilotCli(); + + if (!copilotBin) { + console.error(chalk.red('\n❌ GitHub Copilot CLI not found.\n')); + console.log( + chalk.dim( + ' Install it with:\n' + + ' curl -fsSL https://gh.io/copilot-install | bash\n\n' + + ' GitHub Copilot CLI is a third-party product by GitHub, Inc.\n' + + ' EVODRON is not affiliated with GitHub or Microsoft.\n', + ), + ); + await endSession(sessionId, 0); + process.exit(1); + } + + console.log(chalk.dim(`\n Launching GitHub Copilot CLI [third-party]...\n`)); + + const child = spawn(copilotBin, rawArgs, { + stdio: 'inherit', + env: process.env, + }); + + let commandsCount = 0; + + child.on('close', async (code) => { + commandsCount++; + await endSession(sessionId, commandsCount); + process.exit(code ?? 0); + }); + + child.on('error', async (err) => { + console.error(chalk.red(`\n❌ Failed to launch Copilot CLI: ${err.message}\n`)); + await endSession(sessionId, commandsCount); + process.exit(1); + }); + }); +} + +function findCopilotCli(): string | null { + const { execSync } = require('child_process') as typeof import('child_process'); + + for (const bin of ['ghcs', 'gh']) { + try { + execSync(`which ${bin}`, { stdio: 'ignore' }); + return bin; + } catch { + // not found + } + } + return null; +} + +async function endSession(sessionId: string | undefined, commandsCount: number): Promise { + if (!sessionId) return; + try { + await apiRequest('/sessions/end', { + method: 'POST', + auth: true, + body: JSON.stringify({ sessionId, commandsCount }), + }); + } catch { + // Non-fatal + } +} diff --git a/evodron/packages/cli/src/commands/stats.ts b/evodron/packages/cli/src/commands/stats.ts new file mode 100644 index 00000000..1328a2fd --- /dev/null +++ b/evodron/packages/cli/src/commands/stats.ts @@ -0,0 +1,35 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import { apiRequest } from '../services/api'; +import type { ApiResponse, UserStats } from '@evodron/shared'; + +export function registerStatsCommands(program: Command): void { + program + .command('stats') + .description('Show your EVODRON usage statistics') + .action(async () => { + const res = await apiRequest>('/stats', { auth: true }); + + if (!res.success) { + console.error(chalk.red(`❌ ${res.error}`)); + process.exit(1); + } + + const d = res.data; + + const levelColor = + d.level === 'gold' + ? chalk.yellow + : d.level === 'silver' + ? chalk.gray + : chalk.rgb(205, 127, 50); // bronze + + console.log(chalk.bold('\n📈 Your EVODRON Stats\n')); + console.log(` Level : ${levelColor(d.level.toUpperCase() + ' ★')}`); + console.log(` Total sessions : ${chalk.white(d.totalSessions)}`); + console.log(` Total commands : ${chalk.white(d.totalCommands)}`); + console.log(` Credits : ${chalk.yellow(d.totalCredits + ' ⚡')}`); + console.log(` Member since : ${chalk.dim(new Date(d.joinedAt).toLocaleDateString())}`); + console.log(); + }); +} diff --git a/evodron/packages/cli/src/services/api.ts b/evodron/packages/cli/src/services/api.ts new file mode 100644 index 00000000..4c50149d --- /dev/null +++ b/evodron/packages/cli/src/services/api.ts @@ -0,0 +1,71 @@ +import { loadConfig, getApiUrl, saveConfig } from './config'; + +interface ApiOptions { + auth?: boolean; +} + +export async function apiRequest( + path: string, + options: RequestInit & ApiOptions = {}, +): Promise { + const { auth = false, ...fetchOptions } = options; + const url = `${getApiUrl()}${path}`; + const headers: Record = { + 'Content-Type': 'application/json', + ...(fetchOptions.headers as Record), + }; + + if (auth) { + const config = loadConfig(); + if (!config.accessToken) { + throw new Error('Not logged in. Run: evodron auth login'); + } + headers['Authorization'] = 'Bearer ' + config.accessToken!; + } + + const res = await fetch(url, { ...fetchOptions, headers }); + + // Handle 401 — try to refresh token + if (res.status === 401 && auth) { + const refreshed = await tryRefreshToken(); + if (refreshed) { + const config = loadConfig(); + headers['Authorization'] = 'Bearer ' + config.accessToken!; + const retry = await fetch(url, { ...fetchOptions, headers }); + return retry.json() as Promise; + } else { + throw new Error('Session expired. Please login again: evodron auth login'); + } + } + + return res.json() as Promise; +} + +async function tryRefreshToken(): Promise { + const config = loadConfig(); + if (!config.refreshToken) return false; + + try { + const res = await fetch(`${getApiUrl()}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: config.refreshToken }), + }); + const data = (await res.json()) as { + success: boolean; + data?: { accessToken: string; refreshToken: string }; + }; + + if (data.success && data.data) { + saveConfig({ + ...config, + accessToken: data.data.accessToken, + refreshToken: data.data.refreshToken, + }); + return true; + } + } catch { + // ignore + } + return false; +} diff --git a/evodron/packages/cli/src/services/config.test.ts b/evodron/packages/cli/src/services/config.test.ts new file mode 100644 index 00000000..8eab0657 --- /dev/null +++ b/evodron/packages/cli/src/services/config.test.ts @@ -0,0 +1,63 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { clearConfig, getApiUrl, loadConfig, saveConfig } from './config'; + +function createTempConfigDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), 'evodron-cli-config-')); +} + +describe('CLI config service', () => { + const createdDirs: string[] = []; + + afterEach(() => { + delete process.env['EVODRON_CONFIG_DIR']; + delete process.env['EVODRON_API_URL']; + for (const dir of createdDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('saves and loads credentials from the configured directory', () => { + const dir = createTempConfigDir(); + createdDirs.push(dir); + process.env['EVODRON_CONFIG_DIR'] = dir; + + saveConfig({ + accessToken: 'access-token', + refreshToken: 'refresh-token', + username: 'alice', + userId: 'user-1', + }); + + expect(loadConfig()).toEqual({ + accessToken: 'access-token', + refreshToken: 'refresh-token', + username: 'alice', + userId: 'user-1', + }); + }); + + it('prefers the saved API URL over the environment fallback', () => { + const dir = createTempConfigDir(); + createdDirs.push(dir); + process.env['EVODRON_CONFIG_DIR'] = dir; + process.env['EVODRON_API_URL'] = 'http://env.example.test'; + + saveConfig({ apiUrl: 'http://saved.example.test' }); + + expect(getApiUrl()).toBe('http://saved.example.test'); + }); + + it('clears the persisted config file', () => { + const dir = createTempConfigDir(); + createdDirs.push(dir); + process.env['EVODRON_CONFIG_DIR'] = dir; + + saveConfig({ accessToken: 'token' }); + clearConfig(); + + expect(loadConfig()).toEqual({}); + }); +}); diff --git a/evodron/packages/cli/src/services/config.ts b/evodron/packages/cli/src/services/config.ts new file mode 100644 index 00000000..38d9cbd3 --- /dev/null +++ b/evodron/packages/cli/src/services/config.ts @@ -0,0 +1,49 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +interface Config { + accessToken?: string; + refreshToken?: string; + userId?: string; + username?: string; + apiUrl?: string; +} + +function configDir(): string { + return process.env['EVODRON_CONFIG_DIR'] ?? path.join(os.homedir(), '.evodron'); +} + +function configFile(): string { + return path.join(configDir(), 'config.json'); +} + +export function loadConfig(): Config { + try { + const raw = fs.readFileSync(configFile(), 'utf-8'); + return JSON.parse(raw) as Config; + } catch { + return {}; + } +} + +export function saveConfig(config: Config): void { + const dir = configDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + } + fs.writeFileSync(configFile(), JSON.stringify(config, null, 2), { mode: 0o600 }); +} + +export function getApiUrl(): string { + const config = loadConfig(); + return config.apiUrl ?? process.env['EVODRON_API_URL'] ?? 'http://localhost:3000'; +} + +export function clearConfig(): void { + try { + fs.unlinkSync(configFile()); + } catch { + // ignore + } +} diff --git a/evodron/packages/cli/src/ui/splash.ts b/evodron/packages/cli/src/ui/splash.ts new file mode 100644 index 00000000..cb7a6aea --- /dev/null +++ b/evodron/packages/cli/src/ui/splash.ts @@ -0,0 +1,24 @@ +import chalk from 'chalk'; + +const LOGO = ` + ███████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ██╗ + ██╔════╝██║ ██║██╔═══██╗██╔══██╗██╔══██╗██╔═══██╗████╗ ██║ + █████╗ ██║ ██║██║ ██║██║ ██║██████╔╝██║ ██║██╔██╗ ██║ + ██╔══╝ ╚██╗ ██╔╝██║ ██║██║ ██║██╔══██╗██║ ██║██║╚██╗██║ + ███████╗ ╚████╔╝ ╚██████╔╝██████╔╝██║ ██║╚██████╔╝██║ ╚████║ + ╚══════╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ +`; + +export function splash(): void { + if (process.env['EVODRON_NO_SPLASH'] === '1') return; + process.stdout.write(chalk.cyan(LOGO)); + process.stdout.write(chalk.dim(' AI-powered developer platform ') + chalk.yellow('v0.1.0') + '\n'); + process.stdout.write(chalk.dim(' ─────────────────────────────────────────────────────────────\n')); + process.stdout.write( + chalk.dim( + ' AI features powered by GitHub Copilot CLI (third-party, © GitHub, Inc.)\n', + ), + ); + process.stdout.write(chalk.dim(' EVODRON is not affiliated with GitHub or Microsoft.\n')); + process.stdout.write(chalk.dim(' ─────────────────────────────────────────────────────────────\n\n')); +} diff --git a/evodron/packages/cli/tsconfig.json b/evodron/packages/cli/tsconfig.json new file mode 100644 index 00000000..b224f046 --- /dev/null +++ b/evodron/packages/cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "node16", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "sourceMap": true, + "types": ["node"], + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/evodron/packages/cli/vitest.config.mts b/evodron/packages/cli/vitest.config.mts new file mode 100644 index 00000000..aae509c1 --- /dev/null +++ b/evodron/packages/cli/vitest.config.mts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@evodron/shared': import.meta.dirname + '/../shared/src/index.ts', + }, + }, + test: { + environment: 'node', + globals: false, + testTimeout: 30000, + include: ['src/**/*.test.ts'], + exclude: ['dist/**', 'node_modules/**'], + }, +}); diff --git a/evodron/packages/db/package.json b/evodron/packages/db/package.json new file mode 100644 index 00000000..7a065954 --- /dev/null +++ b/evodron/packages/db/package.json @@ -0,0 +1,25 @@ +{ + "name": "@evodron/db", + "version": "0.1.0", + "description": "EVODRON database schema and migrations", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "migrate": "node dist/migrate.js", + "seed": "node dist/seed.js", + "dev:migrate": "tsx src/migrate.ts", + "dev:seed": "tsx src/seed.ts" + }, + "dependencies": { + "better-sqlite3": "^9.4.3", + "drizzle-orm": "^0.45.2", + "nanoid": "^5.0.4" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.8", + "drizzle-kit": "^0.20.14", + "tsx": "^4.7.1", + "typescript": "^5.3.3" + } +} diff --git a/evodron/packages/db/src/client.ts b/evodron/packages/db/src/client.ts new file mode 100644 index 00000000..207d75f2 --- /dev/null +++ b/evodron/packages/db/src/client.ts @@ -0,0 +1,23 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from './schema'; + +let _db: ReturnType> | null = null; + +export function getDb(dbUrl?: string): ReturnType> { + if (_db) return _db; + + const url = dbUrl ?? process.env['EVODRON_DB_URL'] ?? 'file:./evodron.db'; + // Strip "file:" prefix for better-sqlite3 + const filePath = url.startsWith('file:') ? url.slice(5) : url; + + const sqlite = new Database(filePath); + sqlite.pragma('journal_mode = WAL'); + sqlite.pragma('foreign_keys = ON'); + + _db = drizzle(sqlite, { schema }); + return _db; +} + +export { schema }; +export type Db = ReturnType; diff --git a/evodron/packages/db/src/index.ts b/evodron/packages/db/src/index.ts new file mode 100644 index 00000000..ecaa1564 --- /dev/null +++ b/evodron/packages/db/src/index.ts @@ -0,0 +1,2 @@ +export * from './schema'; +export * from './client'; diff --git a/evodron/packages/db/src/migrate.ts b/evodron/packages/db/src/migrate.ts new file mode 100644 index 00000000..c93a05f0 --- /dev/null +++ b/evodron/packages/db/src/migrate.ts @@ -0,0 +1,107 @@ +/** + * EVODRON database migration runner. + * Creates all tables if they do not exist. + */ +import Database from 'better-sqlite3'; + +const url = process.env['EVODRON_DB_URL'] ?? 'file:./evodron.db'; +const filePath = url.startsWith('file:') ? url.slice(5) : url; + +const db = new Database(filePath); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +const migrations = [ + `CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + referral_code TEXT NOT NULL UNIQUE, + referred_by TEXT REFERENCES users(id), + credits INTEGER NOT NULL DEFAULT 0, + level TEXT NOT NULL DEFAULT 'bronze' CHECK(level IN ('bronze','silver','gold')), + is_admin INTEGER NOT NULL DEFAULT 0, + is_banned INTEGER NOT NULL DEFAULT 0, + consent_metrics INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + started_at TEXT NOT NULL, + ended_at TEXT, + commands_count INTEGER NOT NULL DEFAULT 0, + client_version TEXT, + ip_hash TEXT + )`, + `CREATE TABLE IF NOT EXISTS referrals ( + id TEXT PRIMARY KEY, + referrer_id TEXT NOT NULL REFERENCES users(id), + referee_id TEXT NOT NULL UNIQUE REFERENCES users(id), + status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','active','rewarded','fraud')), + activated_at TEXT, + rewarded_at TEXT, + created_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS rewards ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + type TEXT NOT NULL CHECK(type IN ('referral_active','referral_onboarding','milestone','manual')), + amount INTEGER NOT NULL, + source TEXT NOT NULL, + created_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS reward_rules ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + trigger TEXT NOT NULL, + credits INTEGER NOT NULL, + active INTEGER NOT NULL DEFAULT 1, + description TEXT, + updated_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS abuse_flags ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + reason TEXT NOT NULL, + detail TEXT, + resolved_at TEXT, + flagged_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS ip_registry ( + id TEXT PRIMARY KEY, + ip_hash TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id), + seen_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS refresh_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + token_hash TEXT NOT NULL UNIQUE, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL, + revoked_at TEXT + )`, + // Indexes + `CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_referrals_referrer ON referrals(referrer_id)`, + `CREATE INDEX IF NOT EXISTS idx_rewards_user_id ON rewards(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_abuse_flags_user ON abuse_flags(user_id)`, + `CREATE INDEX IF NOT EXISTS idx_ip_registry_hash ON ip_registry(ip_hash)`, + `CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id)`, + `INSERT OR IGNORE INTO reward_rules (id, name, trigger, credits, active, description, updated_at) VALUES + ('rule-referrer', 'referral_active_referrer', 'referral_active', 100, 1, 'Credits awarded to the referrer when a referee reaches active status', datetime('now')), + ('rule-onboarding', 'referral_onboarding_referee', 'referral_onboarding', 50, 1, 'Onboarding credits awarded to a new user who joined via referral', datetime('now')), + ('rule-milestone-10', 'milestone_10_sessions', 'milestone_10_sessions', 25, 1, 'Bonus credits for completing 10 sessions', datetime('now'))`, +]; + +db.transaction(() => { + for (const sql of migrations) { + db.prepare(sql).run(); + } +})(); + +console.log('✅ EVODRON database migrated successfully'); +db.close(); diff --git a/evodron/packages/db/src/schema.ts b/evodron/packages/db/src/schema.ts new file mode 100644 index 00000000..b727553b --- /dev/null +++ b/evodron/packages/db/src/schema.ts @@ -0,0 +1,101 @@ +import { sqliteTable, text, integer, type AnySQLiteColumn } from 'drizzle-orm/sqlite-core'; + +// ─── users ──────────────────────────────────────────────────────────────────── + +export const users = sqliteTable('users', { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + username: text('username').notNull().unique(), + passwordHash: text('password_hash').notNull(), + referralCode: text('referral_code').notNull().unique(), + referredBy: text('referred_by').references((): AnySQLiteColumn => users.id), + credits: integer('credits').notNull().default(0), + level: text('level', { enum: ['bronze', 'silver', 'gold'] }).notNull().default('bronze'), + isAdmin: integer('is_admin', { mode: 'boolean' }).notNull().default(false), + isBanned: integer('is_banned', { mode: 'boolean' }).notNull().default(false), + consentMetrics: integer('consent_metrics', { mode: 'boolean' }).notNull().default(false), + createdAt: text('created_at').notNull(), + updatedAt: text('updated_at').notNull(), +}); + +// ─── sessions ───────────────────────────────────────────────────────────────── + +export const sessions = sqliteTable('sessions', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + startedAt: text('started_at').notNull(), + endedAt: text('ended_at'), + commandsCount: integer('commands_count').notNull().default(0), + clientVersion: text('client_version'), + ipHash: text('ip_hash'), +}); + +// ─── referrals ──────────────────────────────────────────────────────────────── + +export const referrals = sqliteTable('referrals', { + id: text('id').primaryKey(), + referrerId: text('referrer_id').notNull().references(() => users.id), + refereeId: text('referee_id').notNull().unique().references(() => users.id), + status: text('status', { enum: ['pending', 'active', 'rewarded', 'fraud'] }) + .notNull() + .default('pending'), + activatedAt: text('activated_at'), + rewardedAt: text('rewarded_at'), + createdAt: text('created_at').notNull(), +}); + +// ─── rewards ────────────────────────────────────────────────────────────────── + +export const rewards = sqliteTable('rewards', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + type: text('type', { + enum: ['referral_active', 'referral_onboarding', 'milestone', 'manual'], + }).notNull(), + amount: integer('amount').notNull(), + source: text('source').notNull(), + createdAt: text('created_at').notNull(), +}); + +// ─── reward_rules ───────────────────────────────────────────────────────────── + +export const rewardRules = sqliteTable('reward_rules', { + id: text('id').primaryKey(), + name: text('name').notNull().unique(), + trigger: text('trigger').notNull(), + credits: integer('credits').notNull(), + active: integer('active', { mode: 'boolean' }).notNull().default(true), + description: text('description'), + updatedAt: text('updated_at').notNull(), +}); + +// ─── abuse_flags ────────────────────────────────────────────────────────────── + +export const abuseFlags = sqliteTable('abuse_flags', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + reason: text('reason').notNull(), + detail: text('detail'), + resolvedAt: text('resolved_at'), + flaggedAt: text('flagged_at').notNull(), +}); + +// ─── ip_registry ────────────────────────────────────────────────────────────── + +export const ipRegistry = sqliteTable('ip_registry', { + id: text('id').primaryKey(), + ipHash: text('ip_hash').notNull(), + userId: text('user_id').notNull().references(() => users.id), + seenAt: text('seen_at').notNull(), +}); + +// ─── refresh_tokens ─────────────────────────────────────────────────────────── + +export const refreshTokens = sqliteTable('refresh_tokens', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + tokenHash: text('token_hash').notNull().unique(), + expiresAt: text('expires_at').notNull(), + createdAt: text('created_at').notNull(), + revokedAt: text('revoked_at'), +}); diff --git a/evodron/packages/db/src/seed.ts b/evodron/packages/db/src/seed.ts new file mode 100644 index 00000000..98a49b38 --- /dev/null +++ b/evodron/packages/db/src/seed.ts @@ -0,0 +1,53 @@ +/** + * EVODRON database seeder — populates default reward rules. + */ +import Database from 'better-sqlite3'; +import { randomUUID } from 'crypto'; + +const url = process.env['EVODRON_DB_URL'] ?? 'file:./evodron.db'; +const filePath = url.startsWith('file:') ? url.slice(5) : url; +const db = new Database(filePath); +const now = new Date().toISOString(); + +const defaultRules = [ + { + id: randomUUID(), + name: 'referral_active_referrer', + trigger: 'referral_active', + credits: 100, + active: 1, + description: 'Credits awarded to the referrer when a referee reaches active status', + updated_at: now, + }, + { + id: randomUUID(), + name: 'referral_onboarding_referee', + trigger: 'referral_onboarding', + credits: 50, + active: 1, + description: 'Onboarding credits awarded to a new user who joined via referral', + updated_at: now, + }, + { + id: randomUUID(), + name: 'milestone_10_sessions', + trigger: 'milestone_10_sessions', + credits: 25, + active: 1, + description: 'Bonus credits for completing 10 sessions', + updated_at: now, + }, +]; + +const stmt = db.prepare(` + INSERT OR IGNORE INTO reward_rules (id, name, trigger, credits, active, description, updated_at) + VALUES (@id, @name, @trigger, @credits, @active, @description, @updated_at) +`); + +const insert = db.transaction(() => { + for (const rule of defaultRules) stmt.run(rule); +}); +insert(); + +console.log('✅ Default reward rules seeded'); +db.close(); diff --git a/evodron/packages/db/tsconfig.json b/evodron/packages/db/tsconfig.json new file mode 100644 index 00000000..53d4e1af --- /dev/null +++ b/evodron/packages/db/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "node16", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"], + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/evodron/packages/shared/package.json b/evodron/packages/shared/package.json new file mode 100644 index 00000000..fb5c4d40 --- /dev/null +++ b/evodron/packages/shared/package.json @@ -0,0 +1,17 @@ +{ + "name": "@evodron/shared", + "version": "0.1.0", + "description": "Shared types and schemas for the EVODRON platform", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "typescript": "^5.3.3" + } +} diff --git a/evodron/packages/shared/src/index.ts b/evodron/packages/shared/src/index.ts new file mode 100644 index 00000000..9ea46a12 --- /dev/null +++ b/evodron/packages/shared/src/index.ts @@ -0,0 +1,120 @@ +import { z } from 'zod'; + +// ─── User ──────────────────────────────────────────────────────────────────── + +export const RegisterSchema = z.object({ + email: z.string().email(), + username: z.string().min(3).max(32).regex(/^[a-zA-Z0-9_-]+$/), + password: z.string().min(8).max(128), + referralCode: z.string().length(8).optional(), +}); +export type RegisterInput = z.infer; + +export const LoginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); +export type LoginInput = z.infer; + +export interface UserPublic { + id: string; + email: string; + username: string; + referralCode: string; + credits: number; + level: UserLevel; + createdAt: string; +} + +export type UserLevel = 'bronze' | 'silver' | 'gold'; + +// ─── Session ───────────────────────────────────────────────────────────────── + +export const StartSessionSchema = z.object({ + clientVersion: z.string().optional(), +}); +export type StartSessionInput = z.infer; + +export const EndSessionSchema = z.object({ + sessionId: z.string().uuid(), + commandsCount: z.number().int().min(0), +}); +export type EndSessionInput = z.infer; + +// ─── Referral ──────────────────────────────────────────────────────────────── + +export type ReferralStatus = 'pending' | 'active' | 'rewarded' | 'fraud'; + +export interface ReferralStats { + code: string; + link: string; + totalReferrals: number; + activeReferrals: number; + pendingReferrals: number; + totalCreditsEarned: number; + referrals: ReferralEntry[]; +} + +export interface ReferralEntry { + id: string; + refereeUsername: string; + status: ReferralStatus; + createdAt: string; +} + +// ─── Reward ────────────────────────────────────────────────────────────────── + +export type RewardType = + | 'referral_active' + | 'referral_onboarding' + | 'milestone' + | 'manual'; + +export interface Reward { + id: string; + type: RewardType; + amount: number; + source: string; + createdAt: string; +} + +export interface RewardRule { + id: string; + name: string; + trigger: string; + credits: number; + active: boolean; +} + +// ─── Stats ─────────────────────────────────────────────────────────────────── + +export interface UserStats { + totalSessions: number; + totalCommands: number; + totalCredits: number; + level: UserLevel; + joinedAt: string; +} + +// ─── API Responses ─────────────────────────────────────────────────────────── + +export interface ApiSuccess { + success: true; + data: T; +} + +export interface ApiError { + success: false; + error: string; + code?: string; +} + +export type ApiResponse = ApiSuccess | ApiError; + +// ─── Auth tokens ───────────────────────────────────────────────────────────── + +export interface AuthTokens { + accessToken: string; + refreshToken: string; + expiresIn: number; +} diff --git a/evodron/packages/shared/tsconfig.json b/evodron/packages/shared/tsconfig.json new file mode 100644 index 00000000..53d4e1af --- /dev/null +++ b/evodron/packages/shared/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "node16", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"], + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/evodron/scripts/setup.sh b/evodron/scripts/setup.sh new file mode 100755 index 00000000..490b9d38 --- /dev/null +++ b/evodron/scripts/setup.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# EVODRON Setup Script +# Sets up the EVODRON platform for development. +# Usage: bash scripts/setup.sh + +set -e + +cd "$(dirname "$0")/.." + +echo "" +echo " ███████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ██╗" +echo " ██╔════╝██║ ██║██╔═══██╗██╔══██╗██╔══██╗██╔═══██╗████╗ ██║" +echo " █████╗ ██║ ██║██║ ██║██║ ██║██████╔╝██║ ██║██╔██╗ ██║" +echo " ██╔══╝ ╚██╗ ██╔╝██║ ██║██║ ██║██╔══██╗██║ ██║██║╚██╗██║" +echo " ███████╗ ╚████╔╝ ╚██████╔╝██████╔╝██║ ██║╚██████╔╝██║ ╚████║" +echo " ╚══════╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝" +echo "" +echo " EVODRON Setup" +echo " ─────────────────────────────────────────────────────────────────" +echo "" + +# 1. Check Node.js +if ! command -v node &>/dev/null; then + echo "❌ Node.js is required (>= 18). Please install it first." + exit 1 +fi + +NODE_VER=$(node -e "process.stdout.write(process.versions.node.split('.')[0])") +if [ "$NODE_VER" -lt 18 ]; then + echo "❌ Node.js >= 18 required. Found: $(node --version)" + exit 1 +fi + +echo "✅ Node.js $(node --version) found" + +# 2. Copy .env if not exists +if [ ! -f .env ]; then + cp .env.example .env + echo "✅ .env created from .env.example" + echo "⚠️ Edit .env and set a secure EVODRON_JWT_SECRET before starting the API." +else + echo "✅ .env already exists" +fi + +# 3. Install dependencies +echo "" +echo "📦 Installing dependencies..." +npm install + +# 4. Run database migrations +echo "" +echo "🗄️ Running database migrations..." +npm run db:migrate + +# 5. Seed default reward rules +echo "" +echo "🌱 Seeding default reward rules..." +npm run db:seed + +echo "" +echo "─────────────────────────────────────────────────────────────────" +echo "✅ EVODRON setup complete!" +echo "" +echo " To start the API:" +echo " npm run dev:api" +echo "" +echo " To use the CLI (after build):" +echo " npm run build" +echo " node packages/cli/dist/bin/evodron.js auth register" +echo "" +echo " ⚠️ Remember to set a secure EVODRON_JWT_SECRET in .env!" +echo "─────────────────────────────────────────────────────────────────" +echo "" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..612c4255 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "evodron-repository", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "evodron-repository", + "version": "1.0.0", + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..e22a8b71 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "evodron-repository", + "version": "1.0.0", + "description": "Repository entrypoint for the EVODRON platform workspace", + "private": true, + "scripts": { + "setup": "cd evodron && npm ci && npm run build && npm run db:migrate && npm run db:seed", + "build": "cd evodron && npm run build", + "test": "cd evodron && npm test", + "lint": "cd evodron && npm run lint", + "dev:api": "cd evodron && npm run dev:api", + "dev:cli": "cd evodron && npm run dev:cli", + "db:migrate": "cd evodron && npm run db:migrate", + "db:seed": "cd evodron && npm run db:seed", + "start": "cd evodron && npm run start --workspace=@evodron/api" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 00000000..4fa63942 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,184 @@ +/* ── Evodron Referral — Stylesheet ───────────────────────────────────── */ + +:root { + --color-bg: #0f1117; + --color-surface: #1a1d27; + --color-border: #2a2d3a; + --color-primary: #6e56cf; + --color-primary-h: #7d68d8; + --color-success: #22c55e; + --color-error: #ef4444; + --color-text: #e2e8f0; + --color-muted: #8892a4; + --radius: 10px; + --font: 'Segoe UI', system-ui, sans-serif; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font); + font-size: 15px; + line-height: 1.6; + min-height: 100vh; +} + +a { color: var(--color-primary); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* ── Layout ── */ +.container { max-width: 820px; margin: 0 auto; padding: 0 1.25rem; } + +/* ── Header ── */ +.site-header { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: .75rem 0; +} +.site-header .container { + display: flex; + align-items: center; + justify-content: space-between; +} +.site-header nav { display: flex; gap: .5rem; align-items: center; } +.logo { font-size: 1.25rem; font-weight: 700; color: var(--color-text); } + +/* ── Buttons ── */ +.btn { + display: inline-flex; + align-items: center; + gap: .4rem; + padding: .45rem 1rem; + border: none; + border-radius: var(--radius); + font-size: .9rem; + font-weight: 600; + cursor: pointer; + transition: background .15s, opacity .15s; + text-decoration: none; +} +.btn-primary { background: var(--color-primary); color: #fff; } +.btn-primary:hover { background: var(--color-primary-h); } +.btn-secondary { background: var(--color-surface); color: var(--color-text); border: 1px solid var(--color-border); } +.btn-secondary:hover { background: var(--color-border); } +.btn-ghost { background: transparent; color: var(--color-muted); border: 1px solid var(--color-border); } +.btn-ghost:hover { color: var(--color-text); } +.btn-large { padding: .7rem 1.6rem; font-size: 1rem; } +.btn-block { width: 100%; justify-content: center; } +.btn-sm { padding: .3rem .7rem; font-size: .8rem; } + +/* ── Hero ── */ +.hero { padding: 5rem 0 3rem; text-align: center; } +.hero h2 { font-size: 2.2rem; margin-bottom: 1rem; } +.hero p { color: var(--color-muted); font-size: 1.1rem; margin-bottom: 2rem; } +.hero-content { display: flex; flex-direction: column; align-items: center; gap: .75rem; } + +/* ── Cards ── */ +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1.5rem; + margin-bottom: 1.25rem; +} +.card h3 { font-size: 1.1rem; margin-bottom: 1rem; } +.card h4 { font-size: .95rem; color: var(--color-muted); margin-bottom: .75rem; } + +/* ── Forms ── */ +.form-page { padding: 3rem 0; display: flex; justify-content: center; } +.form-card { width: 100%; max-width: 440px; } +.form-card h2 { margin-bottom: 1.5rem; text-align: center; } +.form-group { margin-bottom: 1rem; } +.form-group label { display: block; margin-bottom: .3rem; font-size: .875rem; color: var(--color-muted); } +.form-group input { + width: 100%; + padding: .55rem .75rem; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: calc(var(--radius) - 2px); + color: var(--color-text); + font-size: .95rem; +} +.form-group input:focus { outline: 2px solid var(--color-primary); border-color: transparent; } +.form-footer { text-align: center; margin-top: 1rem; color: var(--color-muted); font-size: .875rem; } + +/* ── Alerts ── */ +.alert { + padding: .65rem 1rem; + border-radius: var(--radius); + font-size: .875rem; + margin-bottom: .75rem; +} +.alert-error { background: #2d1a1a; color: var(--color-error); border: 1px solid #6b1d1d; } +.alert-success { background: #0f2d1a; color: var(--color-success); border: 1px solid #1a6b3a; } +.hidden { display: none !important; } + +/* ── Dashboard ── */ +.dashboard { padding: 2rem 0; } +.dashboard h2 { font-size: 1.5rem; margin-bottom: 1.5rem; } + +/* ── Referral Section ── */ +.referral-invite { margin-bottom: 1.25rem; } +.referral-link-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .5rem; + margin-top: .4rem; +} +.referral-link-label { color: var(--color-muted); font-size: .875rem; white-space: nowrap; } +.referral-link-value { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 6px; + padding: .3rem .65rem; + font-size: .85rem; + word-break: break-all; + flex: 1; +} +.copy-feedback { margin-top: .4rem; font-size: .8rem; color: var(--color-success); } + +.referral-stats { margin-bottom: 1.25rem; } +.stats-list { list-style: none; display: flex; flex-direction: column; gap: .3rem; } +.stats-list li { color: var(--color-muted); font-size: .9rem; } +.stats-list li strong { color: var(--color-text); } + +.referral-rewards { margin-bottom: 1rem; } +.reward-badge { + display: inline-flex; + align-items: center; + gap: .4rem; + background: #1e1a3a; + border: 1px solid var(--color-primary); + border-radius: 6px; + padding: .4rem .8rem; + font-size: .875rem; + color: var(--color-primary); + margin: .25rem .25rem 0 0; +} +.reward-badge.claimed { opacity: .45; border-color: var(--color-border); color: var(--color-muted); } + +details { margin-top: .75rem; } +details summary { cursor: pointer; color: var(--color-primary); font-size: .875rem; } +#rewards-history { margin-top: .5rem; } + +.referral-cta { margin-top: 1rem; } + +/* ── Referral info page ── */ +.referral-info-page { padding: 2.5rem 0; } +.referral-info-page h2 { font-size: 1.75rem; margin-bottom: .75rem; } +.lead { color: var(--color-muted); font-size: 1rem; margin-bottom: 1.5rem; } +.steps { padding-left: 1.25rem; display: flex; flex-direction: column; gap: .75rem; } +.steps li { font-size: .95rem; } +.conditions-list { padding-left: 1.25rem; display: flex; flex-direction: column; gap: .5rem; } +.conditions-list li { font-size: .9rem; color: var(--color-muted); } +.legal-note { margin-top: 1rem; font-size: .8rem; color: var(--color-muted); font-style: italic; } +.cta-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; } + +/* ── Muted text ── */ +.muted { color: var(--color-muted); font-size: .875rem; } + +/* ── Divider ── */ +hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25rem 0; } diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 00000000..00edc8b3 --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,85 @@ + + + + + + Evodron — Mon espace + + + + + +
+

Mon espace

+ + +
+

🎁 Programme Evodron

+ + +
+

👤 Invitez un ami

+ + +
+ +
+ + +
+

📊 Statistiques

+
    +
  • Invitations envoyées : —
  • +
  • Inscriptions : —
  • +
  • Utilisateurs ayant commencé à utiliser Evodron : —
  • +
  • Récompenses obtenues : —
  • +
+
+ +
+ + +
+

🎁 Récompenses

+
+

Chargement des récompenses…

+
+
+ Voir l'historique complet +
Chargement…
+
+
+ + +
+ + +
+

🚀 Simuler une première utilisation

+

+ En situation réelle, cette action est déclenchée automatiquement après votre première + utilisation active d'Evodron. Elle active votre parrainage et débloque les récompenses. +

+ + +
+
+ + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..e2546332 --- /dev/null +++ b/public/index.html @@ -0,0 +1,45 @@ + + + + + + Evodron — Bienvenue + + + + + +
+
+

Bienvenue sur Evodron

+

Invitez vos amis, collectez des récompenses, et grandissez ensemble.

+ Créer un compte + Découvrir le programme de parrainage +
+
+ + + + diff --git a/public/js/referral.js b/public/js/referral.js new file mode 100644 index 00000000..fe52554e --- /dev/null +++ b/public/js/referral.js @@ -0,0 +1,117 @@ +/* public/js/referral.js — Dashboard logic */ +'use strict'; + +const TOKEN_KEY = 'evodron_token'; + +function getToken() { + return localStorage.getItem(TOKEN_KEY); +} + +function authHeaders() { + return { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() }; +} + +async function apiFetch(path, opts = {}) { + const res = await fetch(path, { headers: authHeaders(), ...opts }); + if (res.status === 401) { + localStorage.removeItem(TOKEN_KEY); + location.href = '/login.html'; + return null; + } + return res.json(); +} + +function renderRewardBadge(r) { + const label = `${r.amount} ${r.reward_type.replace(/_/g, ' ')}`; + const cls = r.status !== 'available' ? 'reward-badge claimed' : 'reward-badge'; + const claim = r.status === 'available' + ? `` + : `(${r.status})`; + return `🎁 ${label} — ${r.reason.replace(/_/g, ' ')} ${claim}`; +} + +async function claimReward(rewardId) { + const data = await apiFetch(`/api/referral/claim-reward/${rewardId}`, { method: 'POST' }); + if (data) loadRewards(); +} + +async function loadReferralLink() { + const data = await apiFetch('/api/referral/link'); + if (!data) return; + document.getElementById('referral-link-display').textContent = data.referral_link; + + document.getElementById('copy-btn').addEventListener('click', () => { + navigator.clipboard.writeText(data.referral_link).then(() => { + const fb = document.getElementById('copy-feedback'); + fb.classList.remove('hidden'); + setTimeout(() => fb.classList.add('hidden'), 2000); + }); + }); +} + +async function loadStats() { + const data = await apiFetch('/api/referral/stats'); + if (!data) return; + document.getElementById('stat-invitations').textContent = data.total_invitations; + document.getElementById('stat-signups').textContent = data.total_signups; + document.getElementById('stat-active').textContent = data.total_active; + document.getElementById('stat-rewarded').textContent = data.total_rewarded; +} + +async function loadRewards() { + const data = await apiFetch('/api/referral/rewards'); + if (!data) return; + + const availEl = document.getElementById('rewards-available'); + const historyEl = document.getElementById('rewards-history'); + + if (data.available.length === 0) { + availEl.innerHTML = '

Aucune récompense disponible pour l\'instant.

'; + } else { + availEl.innerHTML = data.available.map(renderRewardBadge).join(''); + } + + if (data.history.length === 0) { + historyEl.innerHTML = '

Aucun historique.

'; + } else { + historyEl.innerHTML = data.history.map(renderRewardBadge).join(''); + } +} + +async function activateUse() { + const msgEl = document.getElementById('activate-msg'); + const data = await apiFetch('/api/referral/activate-use', { method: 'POST' }); + if (!data) return; + msgEl.classList.remove('hidden', 'alert-error', 'alert-success'); + if (data.already_active) { + msgEl.textContent = 'Votre compte est déjà actif.'; + msgEl.classList.add('alert-success'); + } else if (data.activated) { + msgEl.textContent = '✅ Compte activé ! Les récompenses ont été créditées si vous avez un parrain.'; + msgEl.classList.add('alert-success'); + loadStats(); + loadRewards(); + } else { + msgEl.textContent = 'Une erreur est survenue.'; + msgEl.classList.add('alert-error'); + } +} + +// ── Init ────────────────────────────────────────────────────────────────── +(function init() { + if (!getToken()) { + location.href = '/login.html'; + return; + } + + document.getElementById('nav-logout').addEventListener('click', () => { + localStorage.removeItem(TOKEN_KEY); + location.href = '/'; + }); + + document.getElementById('activate-btn').addEventListener('click', activateUse); + + loadReferralLink(); + loadStats(); + loadRewards(); +})(); diff --git a/public/login.html b/public/login.html new file mode 100644 index 00000000..21c3c8ab --- /dev/null +++ b/public/login.html @@ -0,0 +1,66 @@ + + + + + + Evodron — Connexion + + + + + +
+
+

Se connecter

+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ + + + diff --git a/public/referral.html b/public/referral.html new file mode 100644 index 00000000..b393e51c --- /dev/null +++ b/public/referral.html @@ -0,0 +1,96 @@ + + + + + + Evodron — Programme de Parrainage + + + + + +
+

Programme de Parrainage Evodron

+

+ Invitez vos amis à rejoindre Evodron et recevez des récompenses lorsqu'ils commencent à utiliser la plateforme. +

+ + +
+

🔄 Comment ça fonctionne ?

+
    +
  1. + Partagez votre lien unique
    + Connectez-vous à votre espace et copiez votre lien de parrainage personnel. +
  2. +
  3. + Votre ami s'inscrit via votre lien
    + Il reçoit automatiquement un avantage de bienvenue sur son compte Evodron. +
  4. +
  5. + Il commence à utiliser Evodron
    + Dès sa première utilisation active, vous recevez votre récompense de parrain. +
  6. +
+
+ + +
+

🎁 Récompenses

+
Chargement des conditions…
+
+ + +
+

📋 Conditions du programme

+
    +
  • Chargement…
  • +
+ +
+ + +
+ + + + diff --git a/public/register.html b/public/register.html new file mode 100644 index 00000000..7ecfc4bc --- /dev/null +++ b/public/register.html @@ -0,0 +1,89 @@ + + + + + + Evodron — Inscription + + + + + +
+
+

Créer un compte

+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ + + + diff --git a/src/config/rewards.js b/src/config/rewards.js new file mode 100644 index 00000000..d62472de --- /dev/null +++ b/src/config/rewards.js @@ -0,0 +1,58 @@ +'use strict'; + +/** + * Evodron Referral Program — Rewards Configuration + * + * All reward values and conditions are defined here so that they can be + * changed without touching business-logic code. + * + * IMPORTANT: No financial reward is issued automatically. The reward_type + * must be explicitly set to a non-financial type. A future operator may + * add a 'financial' type only after ensuring compliance with applicable + * laws and platform policies. + */ + +const REWARD_TYPES = { + CREDIT_EVODRON: 'credit_evodron', + FEATURE_UNLOCK: 'feature_unlock', + DISCOUNT: 'discount', + SUBSCRIPTION: 'subscription', + CUSTOM: 'custom', +}; + +/** + * Reward granted to the new user (referee) upon completing registration + * via a referral link. + */ +const WELCOME_BONUS = { + enabled: true, + reward_type: REWARD_TYPES.CREDIT_EVODRON, + amount: 10, // 10 Evodron credits + reason: 'welcome_bonus', +}; + +/** + * Reward granted to the referrer once the referee reaches the activation + * condition defined in ACTIVATION_CONDITION below. + */ +const REFERRER_REWARD = { + enabled: true, + reward_type: REWARD_TYPES.CREDIT_EVODRON, + amount: 20, // 20 Evodron credits + reason: 'referral_activation', +}; + +/** + * Condition that must be met by the referee before the referrer reward + * is unlocked. Supported values: + * 'first_use' — referee must call POST /api/referral/activate-use once + * (triggered by the application after a meaningful action) + */ +const ACTIVATION_CONDITION = 'first_use'; + +module.exports = { + REWARD_TYPES, + WELCOME_BONUS, + REFERRER_REWARD, + ACTIVATION_CONDITION, +}; diff --git a/src/db/database.js b/src/db/database.js new file mode 100644 index 00000000..5a34fd1a --- /dev/null +++ b/src/db/database.js @@ -0,0 +1,30 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const Database = require('better-sqlite3'); + +const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../evodron.db'); +const SCHEMA = path.join(__dirname, 'schema.sql'); + +let db; + +function getDb() { + if (!db) { + db = new Database(DB_PATH); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + const schema = fs.readFileSync(SCHEMA, 'utf8'); + db.exec(schema); + } + return db; +} + +function closeDb() { + if (db) { + db.close(); + db = null; + } +} + +module.exports = { getDb, closeDb }; diff --git a/src/db/schema.sql b/src/db/schema.sql new file mode 100644 index 00000000..79cd79d0 --- /dev/null +++ b/src/db/schema.sql @@ -0,0 +1,59 @@ +-- Evodron Referral Program — Database Schema + +PRAGMA foreign_keys = ON; + +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + username TEXT NOT NULL, + password_hash TEXT NOT NULL, + referral_code TEXT NOT NULL UNIQUE, + referred_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + is_active INTEGER NOT NULL DEFAULT 0, -- 1 once the user has completed first meaningful use + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Referrals table +-- One row per referrer/referee pair; referee_id is UNIQUE (one referrer per user) +CREATE TABLE IF NOT EXISTS referrals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + referrer_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + referee_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'pending' + CHECK(status IN ('pending', 'active', 'rewarded')), + referral_depth INTEGER NOT NULL DEFAULT 1, -- reserved for multi-level support + created_at TEXT NOT NULL DEFAULT (datetime('now')), + activated_at TEXT +); + +-- Rewards table +CREATE TABLE IF NOT EXISTS rewards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + reward_type TEXT NOT NULL + CHECK(reward_type IN ('credit_evodron', 'feature_unlock', 'discount', 'subscription', 'custom')), + amount REAL NOT NULL DEFAULT 0, + reason TEXT NOT NULL + CHECK(reason IN ('referral_activation', 'welcome_bonus', 'custom')), + status TEXT NOT NULL DEFAULT 'available' + CHECK(status IN ('available', 'claimed', 'expired')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + claimed_at TEXT +); + +-- Referral events log (audit trail, extensible) +CREATE TABLE IF NOT EXISTS referral_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + referral_id INTEGER NOT NULL REFERENCES referrals(id) ON DELETE CASCADE, + event_type TEXT NOT NULL + CHECK(event_type IN ('signup', 'first_use', 'reward_granted', 'reward_claimed')), + metadata TEXT, -- JSON string for extensible payload + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_users_referral_code ON users(referral_code); +CREATE INDEX IF NOT EXISTS idx_referrals_referrer_id ON referrals(referrer_id); +CREATE INDEX IF NOT EXISTS idx_rewards_user_id ON rewards(user_id); +CREATE INDEX IF NOT EXISTS idx_referral_events_referral ON referral_events(referral_id); diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100644 index 00000000..78c4a5f8 --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,38 @@ +'use strict'; + +const jwt = require('jsonwebtoken'); + +const JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error('JWT_SECRET environment variable is required'); +} + +const JWT_EXPIRY = process.env.JWT_EXPIRY || '7d'; + +function signToken(payload) { + return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRY }); +} + +function requireAuth(req, res, next) { + const authHeader = req.headers.authorization; + let token; + + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.slice(7); + } else if (req.cookies && req.cookies.token) { + token = req.cookies.token; + } + + if (!token) { + return res.status(401).json({ error: 'Authentication required' }); + } + + try { + req.user = jwt.verify(token, JWT_SECRET); + next(); + } catch { + return res.status(401).json({ error: 'Invalid or expired token' }); + } +} + +module.exports = { signToken, requireAuth }; diff --git a/src/middleware/rateLimiter.js b/src/middleware/rateLimiter.js new file mode 100644 index 00000000..47d74cdf --- /dev/null +++ b/src/middleware/rateLimiter.js @@ -0,0 +1,31 @@ +'use strict'; + +const rateLimit = require('express-rate-limit'); + +/** + * Strict rate limiter for authentication endpoints. + * 10 requests per 15-minute window per IP. + */ +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: { error: 'Too many requests, please try again later.' }, + skip: () => process.env.NODE_ENV === 'test', +}); + +/** + * General API rate limiter. + * 100 requests per 1-minute window per IP. + */ +const apiLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 100, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: { error: 'Too many requests, please try again later.' }, + skip: () => process.env.NODE_ENV === 'test', +}); + +module.exports = { authLimiter, apiLimiter }; diff --git a/src/models/referral.js b/src/models/referral.js new file mode 100644 index 00000000..4dd37864 --- /dev/null +++ b/src/models/referral.js @@ -0,0 +1,80 @@ +'use strict'; + +const { getDb } = require('../db/database'); + +/** + * Create a referral record linking referrer → referee. + * Guards: + * - referrer_id !== referee_id (no self-referral) + * - referee_id UNIQUE constraint in DB prevents duplicate parrain + */ +function createReferral(referrerId, refereeId) { + if (referrerId === refereeId) { + throw new Error('SELF_REFERRAL'); + } + + const db = getDb(); + const stmt = db.prepare(` + INSERT INTO referrals (referrer_id, referee_id, status) + VALUES (?, ?, 'pending') + `); + + const result = stmt.run(referrerId, refereeId); + return findById(result.lastInsertRowid); +} + +function findById(id) { + return getDb() + .prepare('SELECT * FROM referrals WHERE id = ?') + .get(id); +} + +function findByRefereeId(refereeId) { + return getDb() + .prepare('SELECT * FROM referrals WHERE referee_id = ?') + .get(refereeId); +} + +function getReferrerStats(referrerId) { + const db = getDb(); + return { + total_invitations: db.prepare('SELECT COUNT(*) AS n FROM referrals WHERE referrer_id = ?').get(referrerId).n, + total_signups: db.prepare("SELECT COUNT(*) AS n FROM referrals WHERE referrer_id = ? AND status IN ('pending','active','rewarded')").get(referrerId).n, + total_active: db.prepare("SELECT COUNT(*) AS n FROM referrals WHERE referrer_id = ? AND status IN ('active','rewarded')").get(referrerId).n, + total_rewarded: db.prepare("SELECT COUNT(*) AS n FROM referrals WHERE referrer_id = ? AND status = 'rewarded'").get(referrerId).n, + }; +} + +/** + * Mark a referral as active (filleul has completed the activation condition). + * Returns the updated referral row. + */ +function activateReferral(referralId) { + getDb() + .prepare("UPDATE referrals SET status = 'active', activated_at = datetime('now') WHERE id = ? AND status = 'pending'") + .run(referralId); + return findById(referralId); +} + +function markRewarded(referralId) { + getDb() + .prepare("UPDATE referrals SET status = 'rewarded' WHERE id = ?") + .run(referralId); + return findById(referralId); +} + +function logEvent(referralId, eventType, metadata = null) { + getDb() + .prepare('INSERT INTO referral_events (referral_id, event_type, metadata) VALUES (?, ?, ?)') + .run(referralId, eventType, metadata ? JSON.stringify(metadata) : null); +} + +module.exports = { + createReferral, + findById, + findByRefereeId, + getReferrerStats, + activateReferral, + markRewarded, + logEvent, +}; diff --git a/src/models/reward.js b/src/models/reward.js new file mode 100644 index 00000000..22562804 --- /dev/null +++ b/src/models/reward.js @@ -0,0 +1,58 @@ +'use strict'; + +const { getDb } = require('../db/database'); +const { WELCOME_BONUS, REFERRER_REWARD } = require('../config/rewards'); + +function grantWelcomeBonus(userId) { + if (!WELCOME_BONUS.enabled) return null; + return _createReward({ + user_id: userId, + reward_type: WELCOME_BONUS.reward_type, + amount: WELCOME_BONUS.amount, + reason: WELCOME_BONUS.reason, + }); +} + +function grantReferrerReward(referrerId) { + if (!REFERRER_REWARD.enabled) return null; + return _createReward({ + user_id: referrerId, + reward_type: REFERRER_REWARD.reward_type, + amount: REFERRER_REWARD.amount, + reason: REFERRER_REWARD.reason, + }); +} + +function _createReward({ user_id, reward_type, amount, reason }) { + const db = getDb(); + const result = db.prepare(` + INSERT INTO rewards (user_id, reward_type, amount, reason) + VALUES (?, ?, ?, ?) + `).run(user_id, reward_type, amount, reason); + + return db.prepare('SELECT * FROM rewards WHERE id = ?').get(result.lastInsertRowid); +} + +function getRewardsForUser(userId) { + return getDb() + .prepare("SELECT * FROM rewards WHERE user_id = ? ORDER BY created_at DESC") + .all(userId); +} + +function getAvailableRewards(userId) { + return getDb() + .prepare("SELECT * FROM rewards WHERE user_id = ? AND status = 'available' ORDER BY created_at DESC") + .all(userId); +} + +function claimReward(rewardId, userId) { + const db = getDb(); + const reward = db.prepare('SELECT * FROM rewards WHERE id = ? AND user_id = ?').get(rewardId, userId); + if (!reward) throw new Error('REWARD_NOT_FOUND'); + if (reward.status !== 'available') throw new Error('REWARD_NOT_AVAILABLE'); + + db.prepare("UPDATE rewards SET status = 'claimed', claimed_at = datetime('now') WHERE id = ?").run(rewardId); + return db.prepare('SELECT * FROM rewards WHERE id = ?').get(rewardId); +} + +module.exports = { grantWelcomeBonus, grantReferrerReward, getRewardsForUser, getAvailableRewards, claimReward }; diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 00000000..21c81f64 --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,53 @@ +'use strict'; + +const { customAlphabet } = require('nanoid'); +const { getDb } = require('../db/database'); + +// 8-character alphanumeric referral codes (upper-case only for readability) +const generateCode = customAlphabet('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', 8); + +function createUser({ email, username, password_hash, referred_by_user_id = null }) { + const db = getDb(); + const code = generateCode(); + + const stmt = db.prepare(` + INSERT INTO users (email, username, password_hash, referral_code, referred_by_user_id) + VALUES (?, ?, ?, ?, ?) + `); + + const result = stmt.run( + email.toLowerCase().trim(), + username.trim(), + password_hash, + code, + referred_by_user_id + ); + + return findById(result.lastInsertRowid); +} + +function findById(id) { + return getDb() + .prepare('SELECT id, email, username, referral_code, referred_by_user_id, is_active, created_at FROM users WHERE id = ?') + .get(id); +} + +function findByEmail(email) { + return getDb() + .prepare('SELECT * FROM users WHERE email = ?') + .get(email.toLowerCase().trim()); +} + +function findByReferralCode(code) { + return getDb() + .prepare('SELECT id, email, username, referral_code FROM users WHERE referral_code = ?') + .get(code); +} + +function markActive(userId) { + return getDb() + .prepare('UPDATE users SET is_active = 1 WHERE id = ?') + .run(userId); +} + +module.exports = { createUser, findById, findByEmail, findByReferralCode, markActive, generateCode }; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 00000000..39d5d967 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,93 @@ +'use strict'; + +const express = require('express'); +const bcrypt = require('bcryptjs'); +const UserModel = require('../models/user'); +const ReferralModel = require('../models/referral'); +const RewardModel = require('../models/reward'); +const { signToken } = require('../middleware/auth'); + +const router = express.Router(); + +/** + * POST /api/auth/register + * Body: { email, username, password, referral_code? } + */ +router.post('/register', async (req, res) => { + try { + const { email, username, password, referral_code } = req.body; + + if (!email || !username || !password) { + return res.status(400).json({ error: 'email, username and password are required' }); + } + if (password.length < 8) { + return res.status(400).json({ error: 'Password must be at least 8 characters' }); + } + + // Check for duplicate email + if (UserModel.findByEmail(email)) { + return res.status(409).json({ error: 'Email already registered' }); + } + + // Resolve referrer (silent ignore if code is invalid) + let referrer = null; + if (referral_code) { + referrer = UserModel.findByReferralCode(referral_code); + } + + const password_hash = await bcrypt.hash(password, 12); + const newUser = UserModel.createUser({ + email, + username, + password_hash, + referred_by_user_id: referrer ? referrer.id : null, + }); + + // Create referral record and grant welcome bonus + if (referrer && referrer.id !== newUser.id) { + try { + const referral = ReferralModel.createReferral(referrer.id, newUser.id); + ReferralModel.logEvent(referral.id, 'signup'); + RewardModel.grantWelcomeBonus(newUser.id); + } catch (e) { + // SELF_REFERRAL or DB unique violation — silently skip + } + } + + const token = signToken({ id: newUser.id, email: newUser.email }); + return res.status(201).json({ token, user: newUser }); + } catch (err) { + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * POST /api/auth/login + * Body: { email, password } + */ +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: 'email and password are required' }); + } + + const user = UserModel.findByEmail(email); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const token = signToken({ id: user.id, email: user.email }); + const safeUser = UserModel.findById(user.id); + return res.json({ token, user: safeUser }); + } catch { + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/src/routes/health.js b/src/routes/health.js new file mode 100644 index 00000000..490b4557 --- /dev/null +++ b/src/routes/health.js @@ -0,0 +1,29 @@ +'use strict'; + +const express = require('express'); +const { getDb } = require('../db/database'); +const { apiLimiter } = require('../middleware/rateLimiter'); + +const router = express.Router(); + +/** + * GET /api/health + * Returns the application health status. + * No authentication required — used by load balancers and monitoring. + */ +router.get('/', apiLimiter, (req, res) => { + const status = { status: 'ok', version: process.env.npm_package_version || '1.0.0', timestamp: new Date().toISOString() }; + + try { + getDb().prepare('SELECT 1').get(); + status.db = 'ok'; + } catch { + status.db = 'error'; + status.status = 'degraded'; + } + + const code = status.status === 'ok' ? 200 : 503; + return res.status(code).json(status); +}); + +module.exports = router; diff --git a/src/routes/referral.js b/src/routes/referral.js new file mode 100644 index 00000000..d955b7b0 --- /dev/null +++ b/src/routes/referral.js @@ -0,0 +1,119 @@ +'use strict'; + +const express = require('express'); +const UserModel = require('../models/user'); +const ReferralModel = require('../models/referral'); +const RewardModel = require('../models/reward'); +const { requireAuth } = require('../middleware/auth'); +const { ACTIVATION_CONDITION, REFERRER_REWARD, WELCOME_BONUS, REWARD_TYPES } = require('../config/rewards'); + +const router = express.Router(); + +/** + * GET /api/referral/link + * Returns the authenticated user's referral link and code. + */ +router.get('/link', requireAuth, (req, res) => { + const user = UserModel.findById(req.user.id); + if (!user) return res.status(404).json({ error: 'User not found' }); + + const base = process.env.APP_URL || `${req.protocol}://${req.get('host')}`; + const link = `${base}/register?ref=${user.referral_code}`; + + return res.json({ referral_code: user.referral_code, referral_link: link }); +}); + +/** + * GET /api/referral/stats + * Returns referral statistics for the authenticated user. + */ +router.get('/stats', requireAuth, (req, res) => { + const stats = ReferralModel.getReferrerStats(req.user.id); + return res.json(stats); +}); + +/** + * GET /api/referral/rewards + * Returns all rewards for the authenticated user. + */ +router.get('/rewards', requireAuth, (req, res) => { + const all = RewardModel.getRewardsForUser(req.user.id); + const available = RewardModel.getAvailableRewards(req.user.id); + return res.json({ available, history: all }); +}); + +/** + * POST /api/referral/claim-reward/:rewardId + * Claim an available reward. + */ +router.post('/claim-reward/:rewardId', requireAuth, (req, res) => { + try { + const reward = RewardModel.claimReward(Number(req.params.rewardId), req.user.id); + return res.json({ reward }); + } catch (e) { + if (e.message === 'REWARD_NOT_FOUND') return res.status(404).json({ error: 'Reward not found' }); + if (e.message === 'REWARD_NOT_AVAILABLE') return res.status(409).json({ error: 'Reward already claimed or expired' }); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * POST /api/referral/activate-use + * Called by the application when the authenticated user completes their + * first meaningful action. Triggers the referrer reward if applicable. + * + * This endpoint is idempotent: calling it multiple times has no effect + * once the user is already active. + */ +router.post('/activate-use', requireAuth, (req, res) => { + const user = UserModel.findById(req.user.id); + if (!user) return res.status(404).json({ error: 'User not found' }); + + // Already active — nothing to do + if (user.is_active) { + return res.json({ already_active: true }); + } + + UserModel.markActive(user.id); + + // Check if this user was referred + const referral = ReferralModel.findByRefereeId(user.id); + if (referral && referral.status === 'pending') { + ReferralModel.activateReferral(referral.id); + ReferralModel.logEvent(referral.id, 'first_use'); + + // Grant referrer reward + const reward = RewardModel.grantReferrerReward(referral.referrer_id); + if (reward) { + ReferralModel.logEvent(referral.id, 'reward_granted', { reward_id: reward.id }); + ReferralModel.markRewarded(referral.id); + } + } + + return res.json({ activated: true }); +}); + +/** + * GET /api/referral/program-info + * Returns the current program conditions (public, no auth required). + */ +router.get('/program-info', (req, res) => { + return res.json({ + welcome_bonus: WELCOME_BONUS.enabled + ? { type: WELCOME_BONUS.reward_type, amount: WELCOME_BONUS.amount } + : null, + referrer_reward: REFERRER_REWARD.enabled + ? { type: REFERRER_REWARD.reward_type, amount: REFERRER_REWARD.amount } + : null, + activation_condition: ACTIVATION_CONDITION, + reward_types: Object.values(REWARD_TYPES), + conditions: [ + 'Un utilisateur ne peut parrainer qu\'un ami qu\'une seule fois.', + 'L\'auto-parrainage est interdit.', + 'La récompense du parrain est créditée uniquement après la première utilisation active du filleul.', + 'Les récompenses ne sont pas de l\'argent réel et sont soumises aux conditions générales Evodron.', + ], + }); +}); + +module.exports = router; diff --git a/tests/anti-abuse.test.js b/tests/anti-abuse.test.js new file mode 100644 index 00000000..bcff59a4 --- /dev/null +++ b/tests/anti-abuse.test.js @@ -0,0 +1,137 @@ +'use strict'; + +/** + * Tests for Evodron Referral Program — anti-abuse protections. + */ + +process.env.JWT_SECRET = 'test-secret-do-not-use-in-production'; +process.env.DB_PATH = ':memory:'; + +const request = require('supertest'); +const app = require('../app'); + +function bearer(token) { + return 'Bearer ' + token; +} + +async function register(agent, overrides) { + const opts = overrides || {}; + return agent.post('/api/auth/register').send({ + email: opts.email || ('user_' + Date.now() + '_' + Math.random() + '@test.com'), + username: opts.username || 'TestUser', + password: opts.password || 'Password123', + referral_code: opts.referral_code, + }); +} + +async function login(agent, email) { + const res = await agent.post('/api/auth/login').send({ email, password: 'Password123' }); + return res.body.token; +} + +// ── Self-referral ───────────────────────────────────────────────────────── + +describe('Anti-abuse: self-referral', () => { + const agent = request(app); + + test('user cannot use their own referral code (same email → 409 first)', async () => { + const email = 'self_' + Date.now() + '@test.com'; + const firstRes = await register(agent, { email }); + const myCode = firstRes.body.user.referral_code; + + const res2 = await register(agent, { email, referral_code: myCode }); + expect(res2.status).toBe(409); + }); + + test('model-level self-referral guard throws SELF_REFERRAL', () => { + const ReferralModel = require('../src/models/referral'); + expect(() => ReferralModel.createReferral(5, 5)).toThrow('SELF_REFERRAL'); + }); +}); + +// ── Duplicate parrain ───────────────────────────────────────────────────── + +describe('Anti-abuse: duplicate parrain', () => { + const agent = request(app); + + test('a user can only have one referral (DB UNIQUE constraint)', async () => { + const ref1Email = 'ref1_' + Date.now() + '@test.com'; + const ref2Email = 'ref2_' + Date.now() + '@test.com'; + const ref1Res = await register(agent, { email: ref1Email }); + const ref2Res = await register(agent, { email: ref2Email }); + const code1 = ref1Res.body.user.referral_code; + + const newEmail = 'newbie_' + Date.now() + '@test.com'; + await register(agent, { email: newEmail, referral_code: code1 }); + + const UserModel = require('../src/models/user'); + const ReferralModel = require('../src/models/referral'); + + const referee = UserModel.findByEmail(newEmail); + const ref2User = UserModel.findByEmail(ref2Email); + + expect(() => ReferralModel.createReferral(ref2User.id, referee.id)).toThrow(); + }); +}); + +// ── Invalid code ────────────────────────────────────────────────────────── + +describe('Anti-abuse: invalid referral code', () => { + const agent = request(app); + + test('registration with invalid referral code succeeds silently', async () => { + const res = await register(agent, { + email: 'invalid_ref_' + Date.now() + '@test.com', + referral_code: 'XXXXXXXX', + }); + expect(res.status).toBe(201); + expect(res.body.user.referred_by_user_id).toBeNull(); + }); +}); + +// ── Reward timing ───────────────────────────────────────────────────────── + +describe('Anti-abuse: reward not granted without activation', () => { + const agent = request(app); + + test('referrer receives NO referral_activation reward at referee signup', async () => { + const refEmail = 'noreward_' + Date.now() + '@test.com'; + const refRes = await register(agent, { email: refEmail }); + const refCode = refRes.body.user.referral_code; + const refToken = await login(agent, refEmail); + + await register(agent, { email: 'newreferee_' + Date.now() + '@test.com', referral_code: refCode }); + + const rwRes = await agent.get('/api/referral/rewards').set('Authorization', bearer(refToken)); + const activationRewards = rwRes.body.available.filter(r => r.reason === 'referral_activation'); + expect(activationRewards.length).toBe(0); + }); +}); + +// ── Claim isolation ─────────────────────────────────────────────────────── + +describe('Anti-abuse: claim reward isolation', () => { + const agent = request(app); + + test('cannot claim another user\'s reward', async () => { + const refEmail = 'claimref_' + Date.now() + '@test.com'; + const refRes = await register(agent, { email: refEmail }); + const refCode = refRes.body.user.referral_code; + + const email1 = 'claim1_' + Date.now() + '@test.com'; + const email2 = 'claim2_' + Date.now() + '@test.com'; + await register(agent, { email: email1, referral_code: refCode }); + await register(agent, { email: email2 }); + + const token1 = await login(agent, email1); + const token2 = await login(agent, email2); + + const rwRes = await agent.get('/api/referral/rewards').set('Authorization', bearer(token1)); + const reward = rwRes.body.available[0]; + if (!reward) return; + + const claimRes = await agent.post('/api/referral/claim-reward/' + reward.id) + .set('Authorization', bearer(token2)); + expect(claimRes.status).toBe(404); + }); +}); diff --git a/tests/health.test.js b/tests/health.test.js new file mode 100644 index 00000000..3fc59f18 --- /dev/null +++ b/tests/health.test.js @@ -0,0 +1,17 @@ +'use strict'; + +process.env.JWT_SECRET = 'test-secret-do-not-use-in-production'; +process.env.DB_PATH = ':memory:'; + +const request = require('supertest'); +const app = require('../app'); + +describe('GET /api/health', () => { + test('returns 200 with status ok and db ok', async () => { + const res = await request(app).get('/api/health'); + expect(res.status).toBe(200); + expect(res.body.status).toBe('ok'); + expect(res.body.db).toBe('ok'); + expect(res.body).toHaveProperty('timestamp'); + }); +}); diff --git a/tests/referral.test.js b/tests/referral.test.js new file mode 100644 index 00000000..2498a463 --- /dev/null +++ b/tests/referral.test.js @@ -0,0 +1,196 @@ +'use strict'; + +/** + * Tests for the Evodron Referral Program core functionality. + * Uses supertest against the Express app with an in-memory SQLite DB. + */ + +process.env.JWT_SECRET = 'test-secret-do-not-use-in-production'; +process.env.DB_PATH = ':memory:'; + +const request = require('supertest'); +const app = require('../app'); + +// ── Helpers ────────────────────────────────────────────────────────────── + +function bearer(token) { + return 'Bearer ' + token; +} + +async function register(agent, overrides = {}) { + const body = { + email: overrides.email || ('user_' + Date.now() + '_' + Math.random() + '@test.com'), + username: overrides.username || 'TestUser', + password: overrides.password || 'Password123', + ...overrides, + }; + return agent.post('/api/auth/register').send(body); +} + +async function login(agent, email, password) { + const res = await agent.post('/api/auth/login').send({ email, password: password || 'Password123' }); + return res.body.token; +} + +// ── Auth Routes ─────────────────────────────────────────────────────────── + +describe('POST /api/auth/register', () => { + const agent = request(app); + + test('registers a new user successfully', async () => { + const res = await register(agent); + expect(res.status).toBe(201); + expect(res.body).toHaveProperty('token'); + expect(res.body.user).toHaveProperty('referral_code'); + expect(res.body.user.referral_code).toHaveLength(8); + }); + + test('rejects duplicate email', async () => { + const email = 'dup_' + Date.now() + '@test.com'; + await register(agent, { email }); + const res2 = await register(agent, { email }); + expect(res2.status).toBe(409); + }); + + test('rejects short password', async () => { + const res = await register(agent, { password: 'short' }); + expect(res.status).toBe(400); + }); + + test('rejects missing fields', async () => { + const res = await agent.post('/api/auth/register').send({ email: 'x@y.com' }); + expect(res.status).toBe(400); + }); +}); + +describe('POST /api/auth/login', () => { + const agent = request(app); + + test('logs in with correct credentials', async () => { + const email = 'login_' + Date.now() + '@test.com'; + await register(agent, { email }); + const token = await login(agent, email); + expect(typeof token).toBe('string'); + expect(token.length).toBeGreaterThan(10); + }); + + test('rejects wrong password', async () => { + const email = 'wrongpw_' + Date.now() + '@test.com'; + await register(agent, { email }); + const res = await agent.post('/api/auth/login').send({ email, password: 'WrongPass1' }); + expect(res.status).toBe(401); + }); +}); + +// ── Referral Link ───────────────────────────────────────────────────────── + +describe('GET /api/referral/link', () => { + const agent = request(app); + + test('returns referral link for authenticated user', async () => { + const email = 'link_' + Date.now() + '@test.com'; + await register(agent, { email }); + const token = await login(agent, email); + const res = await agent.get('/api/referral/link').set('Authorization', bearer(token)); + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('referral_link'); + expect(res.body.referral_link).toContain('ref='); + }); + + test('returns 401 without token', async () => { + const res = await agent.get('/api/referral/link'); + expect(res.status).toBe(401); + }); +}); + +// ── Stats ───────────────────────────────────────────────────────────────── + +describe('GET /api/referral/stats', () => { + const agent = request(app); + + test('returns zero stats for new user', async () => { + const email = 'stats_' + Date.now() + '@test.com'; + await register(agent, { email }); + const token = await login(agent, email); + const res = await agent.get('/api/referral/stats').set('Authorization', bearer(token)); + expect(res.status).toBe(200); + expect(res.body.total_invitations).toBe(0); + expect(res.body.total_signups).toBe(0); + }); +}); + +// ── Referral flow ───────────────────────────────────────────────────────── + +describe('Referral flow', () => { + const agent = request(app); + + test('referee gets welcome bonus when registering with valid referral code', async () => { + const refEmail = 'referrer_' + Date.now() + '@test.com'; + const refRes = await register(agent, { email: refEmail }); + const refCode = refRes.body.user.referral_code; + + const newEmail = 'referee_' + Date.now() + '@test.com'; + await register(agent, { email: newEmail, referral_code: refCode }); + const newToken = await login(agent, newEmail); + + const rewardRes = await agent.get('/api/referral/rewards').set('Authorization', bearer(newToken)); + expect(rewardRes.status).toBe(200); + expect(rewardRes.body.available.length).toBeGreaterThan(0); + expect(rewardRes.body.available[0].reason).toBe('welcome_bonus'); + }); + + test('referrer stats increment after referee signs up', async () => { + const refEmail = 'referrer2_' + Date.now() + '@test.com'; + const refRes = await register(agent, { email: refEmail }); + const refCode = refRes.body.user.referral_code; + const refToken = await login(agent, refEmail); + + await register(agent, { email: 'referee2_' + Date.now() + '@test.com', referral_code: refCode }); + + const statsRes = await agent.get('/api/referral/stats').set('Authorization', bearer(refToken)); + expect(statsRes.body.total_signups).toBe(1); + }); + + test('referrer gets reward after referee activates', async () => { + const refEmail = 'referrer3_' + Date.now() + '@test.com'; + const refRes = await register(agent, { email: refEmail }); + const refCode = refRes.body.user.referral_code; + const refToken = await login(agent, refEmail); + + const newEmail = 'referee3_' + Date.now() + '@test.com'; + await register(agent, { email: newEmail, referral_code: refCode }); + const newToken = await login(agent, newEmail); + + const activRes = await agent.post('/api/referral/activate-use').set('Authorization', bearer(newToken)); + expect(activRes.status).toBe(200); + expect(activRes.body.activated).toBe(true); + + const rwRes = await agent.get('/api/referral/rewards').set('Authorization', bearer(refToken)); + expect(rwRes.body.available.some(r => r.reason === 'referral_activation')).toBe(true); + + const statsRes = await agent.get('/api/referral/stats').set('Authorization', bearer(refToken)); + expect(statsRes.body.total_rewarded).toBe(1); + }); + + test('activate-use is idempotent', async () => { + const email = 'idem_' + Date.now() + '@test.com'; + await register(agent, { email }); + const token = await login(agent, email); + + await agent.post('/api/referral/activate-use').set('Authorization', bearer(token)); + const res2 = await agent.post('/api/referral/activate-use').set('Authorization', bearer(token)); + expect(res2.status).toBe(200); + expect(res2.body.already_active).toBe(true); + }); +}); + +// ── Program info ────────────────────────────────────────────────────────── + +describe('GET /api/referral/program-info', () => { + test('returns public program info without auth', async () => { + const res = await request(app).get('/api/referral/program-info'); + expect(res.status).toBe(200); + expect(res.body).toHaveProperty('conditions'); + expect(Array.isArray(res.body.conditions)).toBe(true); + }); +});