commit 7e05dd918cfc911e8b8318bccee733bb1bf8c730 Author: Space-Banane Date: Sat Jul 18 20:14:44 2026 +0200 Patchpass V1 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..43b6eb2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +**/node_modules +**/dist +**/build +**/.env +**/*.log +Backend/.data +.git +UI/public/styles.css diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..1943d5f --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,141 @@ +name: Deploy + +on: + push: + branches: ["main", "dev"] + pull_request: + branches: ["main", "dev"] + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 11.5.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install dependencies + run: | + cd Backend && pnpm install + cd ../UI && pnpm install + + - name: Generate Prisma client + run: cd Backend && pnpm generate + env: + DATABASE_URL: postgresql://x:x@localhost/x + + - name: Build backend + run: cd Backend && pnpm run build + + - name: Build UI + run: cd UI && pnpm run build + + test-and-lint: + name: Test & Lint + needs: build + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: patchpass + POSTGRES_PASSWORD: patchpass + POSTGRES_DB: patchpass + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U patchpass" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + TEST_DATABASE_URL: postgresql://patchpass:patchpass@localhost:5432/patchpass + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 11.5.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install dependencies + run: | + cd Backend && pnpm install + cd ../UI && pnpm install + + - name: Generate Prisma client + run: cd Backend && pnpm generate + env: + DATABASE_URL: ${{ env.TEST_DATABASE_URL }} + + - name: Backend lint + run: cd Backend && pnpm lint + + - name: UI lint + run: cd UI && pnpm lint + + - name: Backend tests + run: cd Backend && pnpm test + + push-image: + name: Build and Push Docker Image + needs: [build, test-and-lint] + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Buildx + uses: docker/setup-buildx-action@v3 + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: registry.reversed.dev/patchpass/core + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=prod,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} + type=sha,format=long + labels: | + org.opencontainers.image.title=PatchPass + org.opencontainers.image.description=A human approval layer for AI agents + org.opencontainers.image.vendor=space + + - name: Log in to Harbor + uses: docker/login-action@v3 + with: + registry: registry.reversed.dev + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + push: true + provenance: false + sbom: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7eeb7aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +build/ +.env +*.log +.DS_Store +Backend/.data/ +Backend/prisma/*.db +UI/public/styles.css +coverage/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6440e61 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,147 @@ +# PatchPass – Agent Guidelines + +## What is PatchPass? + +**A human approval layer for AI agents.** Agents submit structured change requests (code diffs, config updates, custom values); humans review and approve them in the browser; agents then verify the platform-signed decision through the API before applying anything. Packaged as a single `docker compose` stack. + +--- + +## Repo layout + +``` +Backend/ Node.js + TypeScript API (rjweb-server) + src/ + index.ts server bootstrap + static UI serving + notFound SPA fallback + lib/ service layer + shared utilities + ChangeRequestService.ts the request lifecycle — REST and MCP both call this + AgentService.ts agent CRUD (human-owned) + Authentication.ts session (human) + API key (agent) resolution + Signing.ts HMAC-SHA256 approval receipts + ChangeNormalization.ts diff normalization + content hashing + Limits.ts rate/pending/agent-count limits + Notifications.ts, WsHub.ts realtime notifications + SystemCrons.ts expiry + auto-delete jobs + DataManager.ts global (admin) settings + mcp/ MCP JSON-RPC server + tools + middlewares/, RouteAuth.ts + routes/ + api/ human-facing REST (auth, account, agents, change-requests, notifications, admin, global) + v1/ agent-facing REST (/v1/change-requests) + mcp.ts MCP endpoint (/mcp) + ws.ts WebSocket (/api/ws/notifications) + tests/ Vitest integration tests + prisma/schema.prisma +UI/ React + TypeScript + Tailwind (Vite), builds to UI/build +Dockerfile +docker-compose.yml +.gitea/workflows/deploy.yml +``` + +--- + +## Stack + +| Layer | Technology | +|---|---| +| Backend | Node.js + TypeScript (strict), rjweb-server | +| ORM | Prisma 7 (`@prisma/adapter-pg`) | +| Database | PostgreSQL | +| Frontend | React + Tailwind CSS (Vite) | +| Package manager | pnpm (Backend and UI are separate pnpm workspaces) | +| Backend tests | Vitest | + +--- + +## Development workflow + +### Branch strategy + +``` +feature/ → dev → main +``` + +### Running locally + +```bash +# Backend +cd Backend +pnpm dev # esbuild + node, http://localhost:5000 + +# UI (separate terminal) +cd UI +pnpm dev # vite on :3000, proxies /api and /v1 to :5000 +``` + +### Before committing + +1. **Backend lint:** `cd Backend && pnpm lint` +2. **Backend tests:** `cd Backend && pnpm test` +3. **UI lint:** `cd UI && pnpm lint` +4. **UI build check:** `cd UI && pnpm build` + +Never commit code that fails lint or tests. + +--- + +## Key rules + +### The service layer is the single source of truth +`ChangeRequestService.ts` contains ALL request lifecycle logic and authorization. The REST routes (`routes/v1`, `routes/api`) and the MCP tools (`lib/mcp/tools`) are thin adapters that call it. **Do not duplicate authorization or business logic** in a route or tool — add it to the service. + +### Request state machine +States: `PENDING`, `CHANGES_REQUESTED`, `APPROVED`, `REJECTED`, `EXPIRED`, `CONSUMED`, `CANCELLED`. +- Decisions are only allowed on `PENDING`. +- Agent updates are only allowed on `PENDING` / `CHANGES_REQUESTED`. +- `REQUEST_CHANGES` requires a comment (≤500 chars) and resets to `PENDING` on agent resubmit (`resubmitted=true`). +- No mutation after `APPROVED`, `REJECTED`, `EXPIRED`, `CONSUMED`, or `CANCELLED`. +- Approvals are single-use: `consume` moves `APPROVED → CONSUMED`. + +### Signing +Approve/reject decisions are signed with HMAC-SHA256 over a fixed canonical field order (see `Signing.ts`). The signature binds the decision to `content_hash`. Never reorder the canonical fields — it breaks every existing signature. + +### Content hashing +`computeContentHash` hashes `{title, description, normalizedChanges}` with key-sorted canonical JSON and LF-normalized diffs. Equivalent inputs (CRLF vs LF, key order) must hash identically; any semantic change must change the hash. Covered by tests. + +### Limits +`Limits.ts` centralizes: 15 requests/agent/hour, 5 agents/human, 1–10 pending/agent (default 5), expiry 60s–12h (default 30 min), auto-delete ≥7 days. Enforce through these helpers, not ad hoc. + +### Auth +- Humans: session cookie (`patchpass_session`), bcrypt passwords, optional TOTP 2FA. +- Agents: one API key each, `x-api-key` header. +Resolution is in `Authentication.ts`; routes use `requireSession` / `requireAgent` / `requireAdmin` from `RouteAuth.ts`. Disabled humans/agents are rejected. + +### Admin +First registered user is `ADMIN`. Global settings (`registration_enabled`, `requests_enabled`) live in `DataManager.ts`. Admin viewing another user's request payload is explicit and audited (`recordAudit`). Never remove the last admin. + +### UTC & pagination +All timestamps are UTC ISO strings at the API boundary. History and admin logs are paginated. + +--- + +## Database migrations – MANDATORY rules + +> **Never use `prisma db push` or `prisma db pull` to evolve the schema.** + +When you change `prisma/schema.prisma`: + +1. Create a migration: `cd Backend && pnpm migrate` (`prisma migrate dev`), give a short slug. +2. Commit the generated `prisma/migrations/_/migration.sql` with the schema change. +3. Never hand-edit an applied `migration.sql` — create a new migration. +4. Production/CI: `pnpm migrate:deploy`. + +Prisma 7: the datasource URL lives in `prisma.config.ts` via `env("DATABASE_URL")`, not in `schema.prisma`. The client uses the `PrismaPg` adapter — do not remove it. Run `pnpm generate` for TS types without a migration. + +--- + +## General coding rules + +- TypeScript strict mode is on. Avoid `any` without reason (it is a lint warning). +- Do not use `console.*` in the Backend. Use `createLogger(component)` from `lib/logger.ts`. +- UI state uses React built-ins (Context + hooks) — no external state library. +- Follow existing conventions: route handlers in `routes/`, shared logic in `lib/`, MCP tools in `lib/mcp/tools/`. + +--- + +## Keeping CLAUDE.md and AGENTS.md in sync – MANDATORY + +`CLAUDE.md` (Claude Code) and `AGENTS.md` (all other AI agents) must always reflect the same rules. Any rule added, removed, or amended in one must be mirrored in the other **in the same commit**. If they drift, fix the gap before continuing. diff --git a/Backend/.eslintrc.js b/Backend/.eslintrc.js new file mode 100644 index 0000000..8cf48c5 --- /dev/null +++ b/Backend/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + }, + plugins: ["@typescript-eslint"], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + root: true, + env: { + node: true, + es2022: true, + }, + rules: { + "no-console": "error", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-require-imports": "error", + }, + ignorePatterns: ["dist/", "node_modules/"], +}; diff --git a/Backend/package.json b/Backend/package.json new file mode 100644 index 0000000..7f1fced --- /dev/null +++ b/Backend/package.json @@ -0,0 +1,46 @@ +{ + "name": "patchpass-backend", + "version": "1.0.0", + "description": "Backend for PatchPass — a human approval layer for AI agents.", + "private": true, + "license": "MIT-0", + "dependencies": { + "@prisma/adapter-pg": "^7.4.0", + "@prisma/client": "^7.4.0", + "@rjweb/runtime-node": "^1.1.1", + "@rjweb/utils": "^1.12.29", + "@types/bcryptjs": "^2.4.6", + "@types/node": "^22.19.15", + "bcryptjs": "^2.4.3", + "dotenv": "17.2.3", + "esbuild": "^0.25.12", + "file-type": "^19.6.0", + "otplib": "^12.0.1", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "pg": "^8.13.1", + "prisma": "^7.4.0", + "rjweb-server": "^9.8.6", + "typescript": "^5.9.3", + "vitest": "^4.1.2", + "zod": "^4.4.3" + }, + "scripts": { + "migrate": "prisma migrate dev", + "migrate:deploy": "prisma migrate deploy", + "generate": "prisma generate", + "build": "rimraf dist && tsc", + "start": "cd dist && node index.js", + "prod": "pnpm build && pnpm start", + "dev": "rimraf dist && esbuild \"src/**/*.ts\" --platform=node --sourcemap --ignore-annotations --format=cjs --target=es2022 --outdir=dist && cd dist && node index.js", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src --ext .ts" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.62.1", + "@typescript-eslint/parser": "^8.62.1", + "eslint": "^8.57.1", + "rimraf": "^5.0.10" + } +} diff --git a/Backend/pnpm-lock.yaml b/Backend/pnpm-lock.yaml new file mode 100644 index 0000000..738549c --- /dev/null +++ b/Backend/pnpm-lock.yaml @@ -0,0 +1,3918 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@prisma/adapter-pg': + specifier: ^7.4.0 + version: 7.8.0 + '@prisma/client': + specifier: ^7.4.0 + version: 7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3) + '@rjweb/runtime-node': + specifier: ^1.1.1 + version: 1.1.1 + '@rjweb/utils': + specifier: ^1.12.29 + version: 1.12.29 + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + dotenv: + specifier: 17.2.3 + version: 17.2.3 + esbuild: + specifier: ^0.25.12 + version: 0.25.12 + file-type: + specifier: ^19.6.0 + version: 19.6.0 + otplib: + specifier: ^12.0.1 + version: 12.0.1 + pg: + specifier: ^8.13.1 + version: 8.22.0 + pino: + specifier: ^10.3.1 + version: 10.3.1 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 + prisma: + specifier: ^7.4.0 + version: 7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3) + rjweb-server: + specifier: ^9.8.6 + version: 9.9.0(@types/node@22.20.1) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.2 + version: 4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0)) + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^8.62.1 + version: 8.64.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.62.1 + version: 8.64.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + +packages: + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@electric-sql/pglite-socket@0.1.1': + resolution: {integrity: sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1': + resolution: {integrity: sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==} + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': + resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/checkbox@5.2.1': + resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.2.2': + resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.1.1': + resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.3': + resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/input@5.1.2': + resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.1.1': + resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.1.1': + resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.3.1': + resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.2.1': + resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.2.1': + resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-default@12.0.1': + resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-v11@12.0.1': + resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@prisma/adapter-pg@7.8.0': + resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==} + + '@prisma/client-runtime-utils@7.8.0': + resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==} + + '@prisma/client@7.8.0': + resolution: {integrity: sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.8.0': + resolution: {integrity: sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.8.0': + resolution: {integrity: sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==} + + '@prisma/dev@0.24.3': + resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} + + '@prisma/driver-adapter-utils@7.8.0': + resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==} + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': + resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==} + + '@prisma/engines@7.8.0': + resolution: {integrity: sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==} + + '@prisma/fetch-engine@7.8.0': + resolution: {integrity: sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.8.0': + resolution: {integrity: sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.2': + resolution: {integrity: sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==} + engines: {bun: '>=1.3.6', node: '>=22.0.0'} + + '@prisma/studio-core@0.27.3': + resolution: {integrity: sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==} + engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@rjweb/runtime-node@1.1.1': + resolution: {integrity: sha512-rlNGXQV3IYn19MsJhnd+tOaEr09vTvRw498ihsKzEDcUsidqN8nYrOS3sRI5I0w6ROY4zyAMsUFbSucktngx1w==} + + '@rjweb/utils@1.12.29': + resolution: {integrity: sha512-iBE0VN4FKYKpMtgrT0KgfscRfIyC6xPnxLF2v/05rSet+bukfqEbfceLfFh5V6U9sH3ncTlZ56kzUsCOvoJsSA==} + engines: {node: '>=18.0.0'} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@typescript-eslint/eslint-plugin@8.64.0': + resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.64.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.64.0': + resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.64.0': + resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.64.0': + resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.64.0': + resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + + better-result@2.10.0: + resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} + + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-copy@4.0.4: + resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-type@19.6.0: + resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} + engines: {node: '>=18'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + giget@3.3.0: + resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + 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 + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.13: + resolution: {integrity: sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + hono@4.12.30: + resolution: {integrity: sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==} + engines: {node: '>=16.9.0'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inquirer@13.4.3: + resolution: {integrity: sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi3-ts@4.6.0: + resolution: {integrity: sha512-a4sfn6L2sIShhtzJqmjGrARvxAW/3F2BJDdyRVvNF9VhAsZSh5hSyI3a9TNvmzBxXmq66nY5LNT5bQcBxYAZZg==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + otplib@12.0.1: + resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + peek-readable@5.4.2: + resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} + engines: {node: '>=14.16'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prisma@7.8.0: + resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + rjweb-server@9.9.0: + resolution: {integrity: sha512-W3Stj8BjgK4vncZTPAdXNSMix4Uq9vCV7T4e/Ec3FHAHNdb6+8xmk3QhqSOYbSpExWAdfitAA3lTDzLSnrYH4w==} + engines: {node: '>=22.0.0'} + hasBin: true + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + strtok3@9.1.1: + resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-arithmetic@0.1.1: + resolution: {integrity: sha512-3VqgsRgzaYfj+zKWn+7O66ifHwbOOnT2BoOrHwdEUBz7az0DetoZOS20+juNJh1klgzvWEi2Qxden41pomOUAQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.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 + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@borewit/text-codec@0.2.2': {} + + '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@hono/node-server@1.19.11(hono@4.12.30)': + dependencies: + hono: 4.12.30 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@inquirer/ansi@2.0.7': {} + + '@inquirer/checkbox@5.2.1(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/confirm@6.1.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/core@11.2.1(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.20.1) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/editor@5.2.2(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/external-editor': 3.0.3(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/expand@5.1.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/external-editor@3.0.3(@types/node@22.20.1)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/input@5.1.2(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/number@4.1.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/password@5.1.1(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/prompts@8.5.2(@types/node@22.20.1)': + dependencies: + '@inquirer/checkbox': 5.2.1(@types/node@22.20.1) + '@inquirer/confirm': 6.1.1(@types/node@22.20.1) + '@inquirer/editor': 5.2.2(@types/node@22.20.1) + '@inquirer/expand': 5.1.1(@types/node@22.20.1) + '@inquirer/input': 5.1.2(@types/node@22.20.1) + '@inquirer/number': 4.1.1(@types/node@22.20.1) + '@inquirer/password': 5.1.1(@types/node@22.20.1) + '@inquirer/rawlist': 5.3.1(@types/node@22.20.1) + '@inquirer/search': 4.2.1(@types/node@22.20.1) + '@inquirer/select': 5.2.1(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/rawlist@5.3.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/search@4.2.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/select@5.2.1(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/type@4.0.7(@types/node@22.20.1)': + optionalDependencies: + '@types/node': 22.20.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@kurkle/color@0.3.4': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + + '@otplib/plugin-thirty-two@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + thirty-two: 1.0.2 + + '@otplib/preset-default@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + + '@otplib/preset-v11@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + + '@oxc-project/types@0.139.0': {} + + '@pinojs/redact@0.4.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@prisma/adapter-pg@7.8.0': + dependencies: + '@prisma/driver-adapter-utils': 7.8.0 + '@types/pg': 8.20.0 + pg: 8.22.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.8.0': {} + + '@prisma/client@7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.8.0 + optionalDependencies: + prisma: 7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.8.0': + dependencies: + c12: 3.3.4 + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.8.0': {} + + '@prisma/dev@0.24.3(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.1 + '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) + '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) + '@hono/node-server': 1.19.11(hono@4.12.30) + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + '@prisma/streams-local': 0.1.2 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.12.30 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {} + + '@prisma/engines@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/fetch-engine': 7.8.0 + '@prisma/get-platform': 7.8.0 + + '@prisma/fetch-engine@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/get-platform': 7.8.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.2': + dependencies: + ajv: 8.20.0 + better-result: 2.10.0 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.27.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + chart.js: 4.5.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-primitive@2.1.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-toggle@1.1.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@rjweb/runtime-node@1.1.1': + dependencies: + '@rjweb/utils': 1.12.29 + bufferutil: 4.1.0 + ws: 8.21.1(bufferutil@4.1.0) + transitivePeerDependencies: + - utf-8-validate + + '@rjweb/utils@1.12.29': + dependencies: + ts-arithmetic: 0.1.1 + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/token@0.3.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/bcryptjs@2.4.6': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 22.20.1 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/type-utils': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.64.0 + eslint: 8.57.1 + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.64.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + + '@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.64.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.64.0': {} + + '@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.64.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.64.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.3': {} + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + atomic-sleep@1.0.0: {} + + aws-ssl-profiles@1.1.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + bcryptjs@2.4.3: {} + + better-result@2.10.0: {} + + brace-expansion@1.1.16: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.0 + giget: 3.3.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + + callsites@3.1.0: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.2.0: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + concat-map@0.0.1: {} + + confbox@0.2.4: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + dateformat@4.6.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + defu@6.1.7: {} + + denque@2.1.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dotenv@17.2.3: {} + + dotenv@17.4.2: {} + + eastasianwidth@0.2.0: {} + + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + env-paths@3.0.0: {} + + es-module-lexer@2.3.1: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.3 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.3.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + exsolve@1.1.0: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-copy@4.0.4: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-uri@3.1.3: {} + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-type@19.6.0: + dependencies: + get-stream: 9.0.1 + strtok3: 9.1.1 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + get-caller-file@2.0.5: {} + + get-port-please@3.2.0: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + giget@3.3.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + graceful-fs@4.2.11: {} + + grammex@3.1.13: {} + + graphemer@1.4.0: {} + + graphmatch@1.1.1: {} + + has-flag@4.0.0: {} + + help-me@5.0.0: {} + + hono@4.12.30: {} + + http-status-codes@2.3.0: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + inquirer@13.4.3(@types/node@22.20.1): + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@22.20.1) + '@inquirer/prompts': 8.5.2(@types/node@22.20.1) + '@inquirer/type': 4.0.7(@types/node@22.20.1) + mute-stream: 3.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.20.1 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-path-inside@3.0.3: {} + + is-property@1.0.2: {} + + is-stream@4.0.1: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.7.0: {} + + joycon@3.1.1: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lru.min@1.1.4: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.16 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + mute-stream@3.0.0: {} + + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + node-gyp-build@4.8.4: {} + + obug@2.1.4: {} + + ohash@2.0.11: {} + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi3-ts@4.6.0: + dependencies: + yaml: 2.9.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + otplib@12.0.1: + dependencies: + '@otplib/core': 12.0.1 + '@otplib/preset-default': 12.0.1 + '@otplib/preset-v11': 12.0.1 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + peek-readable@5.4.2: {} + + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.4 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.0 + pathe: 2.0.3 + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + + prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.8.0 + '@prisma/dev': 0.24.3(typescript@5.9.3) + '@prisma/engines': 7.8.0 + '@prisma/studio-core': 0.27.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + + process-warning@5.0.0: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react@19.2.7: {} + + readdirp@5.0.0: {} + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + remeda@2.33.4: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + + rjweb-server@9.9.0(@types/node@22.20.1): + dependencies: + '@inquirer/prompts': 8.5.2(@types/node@22.20.1) + '@rjweb/utils': 1.12.29 + content-disposition: 0.5.4 + inquirer: 13.4.3(@types/node@22.20.1) + openapi3-ts: 4.6.0 + yargs: 17.7.3 + zod: 4.4.3 + transitivePeerDependencies: + - '@types/node' + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + run-async@4.0.6: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + secure-json-parse@4.1.0: {} + + semver@7.8.5: {} + + seq-queue@0.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + sqlstring@2.3.3: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + std-env@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.3: {} + + strtok3@9.1.1: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.4.2 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + text-table@0.2.0: {} + + thirty-two@1.0.2: {} + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-arithmetic@0.1.1: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.9.3: {} + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + vite@8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.1 + esbuild: 0.25.12 + fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.9.0 + + vitest@4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0)) + '@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.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@22.20.1)(esbuild@0.25.12)(jiti@2.7.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.21.1(bufferutil@4.1.0): + optionalDependencies: + bufferutil: 4.1.0 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.13 + graphmatch: 1.1.1 + + zod@4.4.3: {} diff --git a/Backend/pnpm-workspace.yaml b/Backend/pnpm-workspace.yaml new file mode 100644 index 0000000..86bf373 --- /dev/null +++ b/Backend/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +allowBuilds: + '@prisma/client': true + '@prisma/engines': true + bufferutil: true + esbuild: true + prisma: true diff --git a/Backend/prisma.config.ts b/Backend/prisma.config.ts new file mode 100644 index 0000000..ca63b2d --- /dev/null +++ b/Backend/prisma.config.ts @@ -0,0 +1,12 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/Backend/prisma/migrations/20260718165630_init/migration.sql b/Backend/prisma/migrations/20260718165630_init/migration.sql new file mode 100644 index 0000000..e90d645 --- /dev/null +++ b/Backend/prisma/migrations/20260718165630_init/migration.sql @@ -0,0 +1,182 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER'); + +-- CreateEnum +CREATE TYPE "RequestState" AS ENUM ('PENDING', 'CHANGES_REQUESTED', 'APPROVED', 'REJECTED', 'EXPIRED', 'CONSUMED', 'CANCELLED'); + +-- CreateEnum +CREATE TYPE "NotificationType" AS ENUM ('REQUEST_CREATED', 'REQUEST_UPDATED', 'REQUEST_APPROVED', 'REQUEST_REJECTED', 'CHANGES_REQUESTED', 'REQUEST_CONSUMED', 'REQUEST_CANCELLED', 'REQUEST_EXPIRED', 'AGENT_DISABLED', 'ADMIN_ACTION', 'SETTINGS_CHANGED'); + +-- CreateEnum +CREATE TYPE "GlobalSettingType" AS ENUM ('registration_enabled', 'requests_enabled'); + +-- CreateTable +CREATE TABLE "GlobalSetting" ( + "type" "GlobalSettingType" NOT NULL, + "value" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GlobalSetting_pkey" PRIMARY KEY ("type") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "displayName" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'USER', + "totpSecret" TEXT, + "totpEnabled" BOOLEAN NOT NULL DEFAULT false, + "disabled" BOOLEAN NOT NULL DEFAULT false, + "autoDeleteEnabled" BOOLEAN NOT NULL DEFAULT false, + "autoDeleteDays" INTEGER NOT NULL DEFAULT 30, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" SERIAL NOT NULL, + "hash" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Agent" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "website" TEXT, + "iconUrl" TEXT, + "apiKey" TEXT NOT NULL, + "disabled" BOOLEAN NOT NULL DEFAULT false, + "maxPendingRequests" INTEGER NOT NULL DEFAULT 5, + "ownerId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Agent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ChangeRequest" ( + "id" SERIAL NOT NULL, + "publicId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "changes" JSONB NOT NULL, + "rawChanges" JSONB NOT NULL, + "metadata" JSONB, + "contentHash" TEXT NOT NULL, + "state" "RequestState" NOT NULL DEFAULT 'PENDING', + "expiresAt" TIMESTAMP(3) NOT NULL, + "comment" TEXT, + "decidedAt" TIMESTAMP(3), + "approverId" INTEGER, + "signature" TEXT, + "receiptIssuedAt" TIMESTAMP(3), + "consumedAt" TIMESTAMP(3), + "cancelledAt" TIMESTAMP(3), + "updateCount" INTEGER NOT NULL DEFAULT 0, + "resubmitted" BOOLEAN NOT NULL DEFAULT false, + "lastAgentUpdateAt" TIMESTAMP(3), + "agentId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ChangeRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Notification" ( + "id" SERIAL NOT NULL, + "type" "NotificationType" NOT NULL, + "title" TEXT NOT NULL, + "message" TEXT NOT NULL, + "requestPublicId" TEXT, + "read" BOOLEAN NOT NULL DEFAULT false, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" SERIAL NOT NULL, + "action" TEXT NOT NULL, + "detail" TEXT, + "targetType" TEXT, + "targetId" TEXT, + "actorId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_hash_key" ON "Session"("hash"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Agent_apiKey_key" ON "Agent"("apiKey"); + +-- CreateIndex +CREATE INDEX "Agent_ownerId_idx" ON "Agent"("ownerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ChangeRequest_publicId_key" ON "ChangeRequest"("publicId"); + +-- CreateIndex +CREATE INDEX "ChangeRequest_userId_state_idx" ON "ChangeRequest"("userId", "state"); + +-- CreateIndex +CREATE INDEX "ChangeRequest_agentId_state_idx" ON "ChangeRequest"("agentId", "state"); + +-- CreateIndex +CREATE INDEX "ChangeRequest_createdAt_idx" ON "ChangeRequest"("createdAt"); + +-- CreateIndex +CREATE INDEX "Notification_userId_read_idx" ON "Notification"("userId", "read"); + +-- CreateIndex +CREATE INDEX "Notification_userId_createdAt_idx" ON "Notification"("userId", "createdAt"); + +-- CreateIndex +CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt"); + +-- CreateIndex +CREATE INDEX "AuditLog_actorId_idx" ON "AuditLog"("actorId"); + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Agent" ADD CONSTRAINT "Agent_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ChangeRequest" ADD CONSTRAINT "ChangeRequest_approverId_fkey" FOREIGN KEY ("approverId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ChangeRequest" ADD CONSTRAINT "ChangeRequest_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ChangeRequest" ADD CONSTRAINT "ChangeRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/Backend/prisma/migrations/migration_lock.toml b/Backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/Backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/Backend/prisma/schema.prisma b/Backend/prisma/schema.prisma new file mode 100644 index 0000000..c58d43e --- /dev/null +++ b/Backend/prisma/schema.prisma @@ -0,0 +1,214 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" +} + +// ───────────────────────────────────────────────────────────────────────────── +// Enums +// ───────────────────────────────────────────────────────────────────────────── + +enum UserRole { + ADMIN + USER +} + +enum RequestState { + PENDING + CHANGES_REQUESTED + APPROVED + REJECTED + EXPIRED + CONSUMED + CANCELLED +} + +enum NotificationType { + REQUEST_CREATED + REQUEST_UPDATED + REQUEST_APPROVED + REQUEST_REJECTED + CHANGES_REQUESTED + REQUEST_CONSUMED + REQUEST_CANCELLED + REQUEST_EXPIRED + AGENT_DISABLED + ADMIN_ACTION + SETTINGS_CHANGED +} + +enum GlobalSettingType { + registration_enabled + requests_enabled +} + +// ───────────────────────────────────────────────────────────────────────────── +// Models +// ───────────────────────────────────────────────────────────────────────────── + +/// A global (admin-only) platform setting, keyed by type. +model GlobalSetting { + type GlobalSettingType @id + value String + updatedAt DateTime @updatedAt +} + +/// A human account. The first registered user becomes ADMIN. +model User { + id Int @id @default(autoincrement()) + /// Normalized (lowercased) username — enforces case-insensitive uniqueness. + username String @unique + /// Original-cased display handle shown in the UI. + displayName String + password String + role UserRole @default(USER) + + // Optional TOTP two-factor auth + totpSecret String? + totpEnabled Boolean @default(false) + + // Admin can disable a human account entirely + disabled Boolean @default(false) + + // Auto-delete of old change requests (opt-in, min 7 days, default 30) + autoDeleteEnabled Boolean @default(false) + autoDeleteDays Int @default(30) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sessions Session[] + agents Agent[] + changeRequests ChangeRequest[] @relation("Owner") + decisions ChangeRequest[] @relation("Approver") + notifications Notification[] + auditLogs AuditLog[] @relation("AuditActor") +} + +/// A browser session for a human account (cookie-based auth). +model Session { + id Int @id @default(autoincrement()) + hash String @unique + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([userId]) +} + +/// An AGENT account. Fully managed by a human owner. Holds exactly one API key. +model Agent { + id Int @id @default(autoincrement()) + name String + description String? + website String? + iconUrl String? + /// The single API key the agent authenticates with. + apiKey String @unique + disabled Boolean @default(false) + /// Max simultaneous pending requests (1..10, human-configurable, default 5). + maxPendingRequests Int @default(5) + + ownerId Int + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + changeRequests ChangeRequest[] + + @@index([ownerId]) +} + +/// A structured change request submitted by an agent for human approval. +model ChangeRequest { + id Int @id @default(autoincrement()) + /// Public UUID (v4) exposed to agents as request_id. + publicId String @unique @default(uuid()) + + title String + description String? + /// Normalized changes array (diffs normalized, canonical ordering). + changes Json + /// Original changes exactly as submitted (audit trail). + rawChanges Json + metadata Json? + /// SHA-256 hash over the canonical (title + description + normalized changes). + contentHash String + + state RequestState @default(PENDING) + + expiresAt DateTime + + // Decision fields + comment String? + decidedAt DateTime? + approverId Int? + approver User? @relation("Approver", fields: [approverId], references: [id], onDelete: SetNull) + /// HMAC-SHA256 signature of the approval receipt (platform-signed decision). + signature String? + receiptIssuedAt DateTime? + + consumedAt DateTime? + cancelledAt DateTime? + + // Update tracking for the CHANGES_REQUESTED → resubmit loop + updateCount Int @default(0) + /// True after an agent resubmits following a CHANGES_REQUESTED decision. + resubmitted Boolean @default(false) + lastAgentUpdateAt DateTime? + + agentId Int + agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade) + + // The human who owns/reviews this request (the agent's owner at creation time). + userId Int + user User @relation("Owner", fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId, state]) + @@index([agentId, state]) + @@index([createdAt]) +} + +/// An in-app notification for a human account. +model Notification { + id Int @id @default(autoincrement()) + type NotificationType + title String + message String + /// Optional link to a change request (publicId). + requestPublicId String? + read Boolean @default(false) + + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + + @@index([userId, read]) + @@index([userId, createdAt]) +} + +/// An audit log entry. Records privileged/admin actions and payload views. +model AuditLog { + id Int @id @default(autoincrement()) + action String + /// Human-readable detail / JSON string of context. + detail String? + /// Optional target references. + targetType String? + targetId String? + + actorId Int? + actor User? @relation("AuditActor", fields: [actorId], references: [id], onDelete: SetNull) + + createdAt DateTime @default(now()) + + @@index([createdAt]) + @@index([actorId]) +} diff --git a/Backend/src/index.ts b/Backend/src/index.ts new file mode 100644 index 0000000..56f7379 --- /dev/null +++ b/Backend/src/index.ts @@ -0,0 +1,114 @@ +import { env } from "./lib/env"; // must be first — loads dotenv and validates +import { Server } from "rjweb-server"; +import { Runtime } from "@rjweb/runtime-node"; +import { existsSync, statSync } from "node:fs"; +import { join, normalize } from "path"; +import { prisma } from "./lib/db"; +import { logger } from "./lib/logger"; +import { corsMiddleware, initCorsDomains } from "./lib/middlewares/cors"; +import { mainMiddleware } from "./lib/middlewares/main"; +import { authResolutionMiddleware } from "./lib/middlewares/auth"; +import { makeResponse } from "./lib/response"; +import { ERROR_MESSAGES } from "./lib/errors"; +import { startSystemCrons } from "./lib/SystemCrons"; + +export const VERSION = { + type: "PatchPass API" as const, + major: 1, + minor: 0, + patch: 0, + toString() { + return `${this.major}.${this.minor}.${this.patch}`; + }, +}; + +export const UI_URL = env.UI_URL; +export const REACT_APP_API_URL = env.REACT_APP_API_URL; +export const DOMAIN = env.DOMAIN; +export const PORT = env.PORT; + +export { prisma }; + +const CORS_DOMAINS = env.CORS_URLS.split(",").map((s) => s.trim()); +CORS_DOMAINS.push(UI_URL); +CORS_DOMAINS.push(REACT_APP_API_URL.replace(/\/+$/, "")); +initCorsDomains(CORS_DOMAINS); + +if (env.NODE_ENV !== "test") { + logger.info(`PatchPass API reachable on ${env.PORT}; ${env.REACT_APP_API_URL}`); +} + +const uiBuildPath = join(__dirname, "../../UI/build"); +const uiIndexPath = join(uiBuildPath, "index.html"); +const hasUiBuild = existsSync(uiBuildPath); +const hasUiIndex = existsSync(uiIndexPath); + +export const server = new Server( + Runtime, + { + port: env.PORT, + bind: "0.0.0.0", + version: false, + performance: { lastModified: false, eTag: false }, + logging: { warn: true, debug: false, error: true }, + }, + [ + corsMiddleware.use({}), + mainMiddleware.use({}), + authResolutionMiddleware.use({}), + ], +); + +const loader = new server.FileLoader("/"); +if (env.NODE_ENV !== "test") { + loader.load("./routes", { fileBasedRouting: false }); +} +export const fileRouter = loader.export(); + +/** Resolve a request path to a real file inside the UI build, guarding traversal. */ +function resolveStaticFile(urlPath: string): string | null { + if (!hasUiBuild) return null; + const decoded = decodeURIComponent(urlPath.split("?")[0]); + const candidate = normalize(join(uiBuildPath, decoded)); + if (candidate !== uiBuildPath && !candidate.startsWith(uiBuildPath)) return null; // traversal guard + if (existsSync(candidate) && statSync(candidate).isFile()) return candidate; + return null; +} + +server.notFound(async (ctr) => { + const path = ctr.url.path; + if (path.startsWith("/api") || path.startsWith("/v1") || path === "/mcp") { + return makeResponse({ + ctr, + content: { code: ERROR_MESSAGES.NOT_FOUND.code, message: ERROR_MESSAGES.NOT_FOUND.message }, + }); + } + + // Serve a matching build file (assets, favicons, etc.) if present. + const file = resolveStaticFile(path); + if (file) return ctr.status(200, "OK").printFile(file, { addTypes: true }); + + // SPA fallback — hand any other route to the React app. + if (hasUiIndex) return ctr.status(200, "OK").printFile(uiIndexPath, { addTypes: true }); + + return makeResponse({ + ctr, + content: { code: ERROR_MESSAGES.NOT_FOUND.code, message: ERROR_MESSAGES.NOT_FOUND.message }, + }); +}); + +server.error("httpRequest", async (ctr, error) => { + logger.error(error, "Unhandled HTTP request error"); + return makeResponse({ ctr, content: { code: ERROR_MESSAGES.INTERNAL_SERVER_ERROR.code } }); +}); + +if (env.NODE_ENV !== "test") { + server + .start() + .then(async (port) => { + await prisma.$connect(); + logger.info({ port }, "PatchPass API running"); + startSystemCrons({ prisma }); + }) + .catch((err) => logger.error(err, "Server failed to start")); +} diff --git a/Backend/src/lib/AgentService.ts b/Backend/src/lib/AgentService.ts new file mode 100644 index 0000000..b1475dd --- /dev/null +++ b/Backend/src/lib/AgentService.ts @@ -0,0 +1,165 @@ +import type { Agent, User } from "@prisma/client"; +import { prisma } from "./db"; +import { generateAgentApiKey } from "./Authentication"; +import { validateIconUrl } from "./IconValidation"; +import { checkAgentCountLimit, clampPendingLimit } from "./Limits"; +import { ServiceResult } from "./ChangeRequestService"; + +const ok = (data: T): ServiceResult => ({ ok: true, data }); +const err = (status: number, message: string): ServiceResult => ({ + ok: false, + status, + message, +}); + +export function serializeAgent(agent: Agent, opts: { includeKey?: boolean } = {}) { + return { + id: agent.id, + name: agent.name, + description: agent.description, + website: agent.website, + icon_url: agent.iconUrl, + disabled: agent.disabled, + max_pending_requests: agent.maxPendingRequests, + created_at: agent.createdAt.toISOString(), + updated_at: agent.updatedAt.toISOString(), + ...(opts.includeKey ? { api_key: agent.apiKey } : { api_key_masked: maskKey(agent.apiKey) }), + }; +} + +function maskKey(key: string): string { + if (key.length <= 12) return key; + return key.slice(0, 12) + "…" + key.slice(-4); +} + +export type AgentInput = { + name?: string; + description?: string | null; + website?: string | null; + icon_url?: string | null; + max_pending_requests?: number; +}; + +async function validateInputIcon(iconUrl: string | null | undefined): Promise> { + if (!iconUrl) return ok(null); + const result = await validateIconUrl(iconUrl); + if (!result.valid) return err(400, result.reason); + return ok(null); +} + +export async function listAgents(user: User) { + const agents = await prisma.agent.findMany({ + where: { ownerId: user.id }, + orderBy: { createdAt: "asc" }, + }); + // Attach pending counts so the UI can show queue pressure. + const withCounts = await Promise.all( + agents.map(async (agent) => { + const pending = await prisma.changeRequest.count({ + where: { agentId: agent.id, state: "PENDING" }, + }); + return { ...serializeAgent(agent), pending_count: pending }; + }), + ); + return withCounts; +} + +export async function createAgent(user: User, input: AgentInput): Promise> { + const name = input.name?.trim(); + if (!name || name.length < 1 || name.length > 128) { + return err(400, "Agent name must be 1–128 characters"); + } + + const limit = await checkAgentCountLimit(user.id); + if (!limit.allowed) return err(409, limit.reason); + + const iconCheck = await validateInputIcon(input.icon_url); + if (!iconCheck.ok) return iconCheck; + + const agent = await prisma.agent.create({ + data: { + name, + description: input.description?.trim() || null, + website: input.website?.trim() || null, + iconUrl: input.icon_url?.trim() || null, + apiKey: generateAgentApiKey(), + maxPendingRequests: clampPendingLimit( + input.max_pending_requests ?? 5, + ), + ownerId: user.id, + }, + }); + + // Return the full key exactly once, on creation. + return ok(serializeAgent(agent, { includeKey: true })); +} + +export async function updateAgent( + user: User, + agentId: number, + input: AgentInput, +): Promise> { + const agent = await prisma.agent.findFirst({ where: { id: agentId, ownerId: user.id } }); + if (!agent) return err(404, "Agent not found"); + + if (input.name !== undefined) { + const name = input.name.trim(); + if (name.length < 1 || name.length > 128) { + return err(400, "Agent name must be 1–128 characters"); + } + } + + if (input.icon_url) { + const iconCheck = await validateInputIcon(input.icon_url); + if (!iconCheck.ok) return iconCheck; + } + + const updated = await prisma.agent.update({ + where: { id: agent.id }, + data: { + ...(input.name !== undefined ? { name: input.name.trim() } : {}), + ...(input.description !== undefined ? { description: input.description?.trim() || null } : {}), + ...(input.website !== undefined ? { website: input.website?.trim() || null } : {}), + ...(input.icon_url !== undefined ? { iconUrl: input.icon_url?.trim() || null } : {}), + ...(input.max_pending_requests !== undefined + ? { maxPendingRequests: clampPendingLimit(input.max_pending_requests) } + : {}), + }, + }); + + return ok(serializeAgent(updated)); +} + +export async function regenerateApiKey( + user: User, + agentId: number, +): Promise> { + const agent = await prisma.agent.findFirst({ where: { id: agentId, ownerId: user.id } }); + if (!agent) return err(404, "Agent not found"); + const updated = await prisma.agent.update({ + where: { id: agent.id }, + data: { apiKey: generateAgentApiKey() }, + }); + return ok(serializeAgent(updated, { includeKey: true })); +} + +export async function setAgentDisabled( + user: User, + agentId: number, + disabled: boolean, +): Promise> { + const agent = await prisma.agent.findFirst({ where: { id: agentId, ownerId: user.id } }); + if (!agent) return err(404, "Agent not found"); + const updated = await prisma.agent.update({ + where: { id: agent.id }, + data: { disabled }, + }); + return ok(serializeAgent(updated)); +} + +export async function deleteAgent(user: User, agentId: number): Promise> { + const agent = await prisma.agent.findFirst({ where: { id: agentId, ownerId: user.id } }); + if (!agent) return err(404, "Agent not found"); + await prisma.agent.delete({ where: { id: agent.id } }); + return ok({ deleted: true }); +} diff --git a/Backend/src/lib/Audit.ts b/Backend/src/lib/Audit.ts new file mode 100644 index 0000000..c89a95a --- /dev/null +++ b/Backend/src/lib/Audit.ts @@ -0,0 +1,26 @@ +import { prisma } from "./db"; +import { createLogger } from "./logger"; + +const log = createLogger("AUDIT"); + +export async function recordAudit(input: { + actorId: number | null; + action: string; + detail?: string | null; + targetType?: string | null; + targetId?: string | null; +}) { + try { + await prisma.auditLog.create({ + data: { + actorId: input.actorId, + action: input.action, + detail: input.detail ?? null, + targetType: input.targetType ?? null, + targetId: input.targetId ?? null, + }, + }); + } catch (err) { + log.warn({ err, action: input.action }, "Failed to write audit log"); + } +} diff --git a/Backend/src/lib/Authentication.ts b/Backend/src/lib/Authentication.ts new file mode 100644 index 0000000..c533321 --- /dev/null +++ b/Backend/src/lib/Authentication.ts @@ -0,0 +1,80 @@ +import { createHash, randomBytes } from "crypto"; +import type { Agent, Session, User } from "@prisma/client"; +import { prisma } from "./db"; +import { AGENT_KEY_PREFIX } from "./static"; + +export type AuthState = + | { success: true; method: "session"; user: User; session: Session } + | { success: true; method: "agent"; agent: Agent; owner: User } + | { success: false; message: string; method: "none" }; + +/** + * Resolve authentication from a human session cookie hash and/or an agent API key. + * Sessions win over API keys when both are present. Disabled humans and disabled + * agents (or agents whose owner is disabled) are rejected. + */ +export async function checkAuthentication( + sessionHash: string | null | undefined, + apiKey: string | null | undefined, +): Promise { + if (!sessionHash && !apiKey) { + return { success: false, message: "No authentication data provided", method: "none" }; + } + + if (sessionHash) { + const session = await prisma.session.findFirst({ + where: { hash: sessionHash }, + include: { user: true }, + }); + if (!session) { + return { success: false, message: "Invalid session", method: "none" }; + } + if (session.user.disabled) { + return { success: false, message: "This account has been disabled", method: "none" }; + } + return { success: true, method: "session", user: session.user, session }; + } + + // API key path + const agent = await prisma.agent.findFirst({ + where: { apiKey: apiKey! }, + include: { owner: true }, + }); + if (!agent) { + return { success: false, message: "Invalid API key", method: "none" }; + } + if (agent.disabled) { + return { success: false, message: "This agent has been disabled", method: "none" }; + } + if (agent.owner.disabled) { + return { success: false, message: "The owning account has been disabled", method: "none" }; + } + return { success: true, method: "agent", agent, owner: agent.owner }; +} + +/** Generate a fresh, prefixed agent API key. */ +export function generateAgentApiKey(): string { + return AGENT_KEY_PREFIX + randomBytes(32).toString("hex"); +} + +/** Generate a session hash for a human login. */ +export function generateSessionHash(username: string): string { + return createHash("sha256") + .update(`${Date.now()}+${username}+${randomBytes(16).toString("hex")}`) + .digest("hex"); +} + +/** Normalize a username for case-insensitive storage & lookup. */ +export function normalizeUsername(username: string): string { + return username.trim().toLowerCase(); +} + +/** Compute the cookie domain scope from the configured DOMAIN. */ +export function cookieDomain(domain: string): string { + if (domain === "localhost") return "localhost"; + if (domain.split(".").length > 2) { + // Subdomain deployment: scope to the registrable parent domain. + return domain.split(".").slice(1).join("."); + } + return "." + domain; +} diff --git a/Backend/src/lib/ChangeNormalization.ts b/Backend/src/lib/ChangeNormalization.ts new file mode 100644 index 0000000..152abfe --- /dev/null +++ b/Backend/src/lib/ChangeNormalization.ts @@ -0,0 +1,115 @@ +import { createHash } from "crypto"; + +// ───────────────────────────────────────────────────────────────────────────── +// Change payload types +// +// Agents submit a `changes` array. Each entry is one of three shapes: +// - unified_diff : a git-style unified diff applied with `patch -p1` +// - config : a keyed config value change (before -> after) with a type +// - custom : an arbitrary labelled before/after value +// ───────────────────────────────────────────────────────────────────────────── + +export type UnifiedDiffChange = { + type: "unified_diff"; + path: string; + content: string; +}; + +export type ConfigChange = { + type: "config"; + path: string; + before?: unknown; + after?: unknown; + content_type?: string; +}; + +export type CustomChange = { + type: "custom"; + label: string; + before?: unknown; + after?: unknown; +}; + +export type Change = UnifiedDiffChange | ConfigChange | CustomChange; + +/** + * Normalize a single unified diff: + * - Convert CRLF / lone CR line endings to LF + * - Strip trailing whitespace from the very end, then guarantee exactly one + * trailing newline so hashes are stable regardless of how the agent framed it. + */ +export function normalizeDiff(content: string): string { + const lf = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + const trimmedEnd = lf.replace(/\n+$/, ""); + return trimmedEnd.length > 0 ? `${trimmedEnd}\n` : ""; +} + +/** + * Normalize the full changes array. Diffs get line-ending normalization; config + * and custom entries are returned in a canonical key order. The relative order of + * entries as submitted is preserved (it is semantically meaningful). + */ +export function normalizeChanges(changes: Change[]): Change[] { + return changes.map((change) => { + if (change.type === "unified_diff") { + return { + type: "unified_diff" as const, + path: change.path, + content: normalizeDiff(change.content), + }; + } + if (change.type === "config") { + return { + type: "config" as const, + path: change.path, + before: change.before ?? null, + after: change.after ?? null, + content_type: change.content_type ?? null, + } as ConfigChange; + } + return { + type: "custom" as const, + label: change.label, + before: change.before ?? null, + after: change.after ?? null, + } as CustomChange; + }); +} + +/** + * Produce a canonical, key-sorted JSON string for a value. Used so the content + * hash is independent of key insertion order in the incoming JSON. + */ +export function canonicalize(value: unknown): string { + return JSON.stringify(sortKeys(value)); +} + +function sortKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortKeys); + if (value && typeof value === "object") { + const obj = value as Record; + const out: Record = {}; + for (const key of Object.keys(obj).sort()) { + out[key] = sortKeys(obj[key]); + } + return out; + } + return value; +} + +/** + * Compute the content hash that binds an approval receipt to the exact reviewed + * content. Any change to title, description, or the normalized changes changes it. + */ +export function computeContentHash(input: { + title: string; + description?: string | null; + changes: Change[]; +}): string { + const canonical = canonicalize({ + title: input.title, + description: input.description ?? null, + changes: normalizeChanges(input.changes), + }); + return createHash("sha256").update(canonical).digest("hex"); +} diff --git a/Backend/src/lib/ChangeRequestService.ts b/Backend/src/lib/ChangeRequestService.ts new file mode 100644 index 0000000..20c0e2d --- /dev/null +++ b/Backend/src/lib/ChangeRequestService.ts @@ -0,0 +1,448 @@ +import type { Agent, ChangeRequest, User } from "@prisma/client"; +import { prisma } from "./db"; +import { env } from "./env"; +import { Change, computeContentHash, normalizeChanges } from "./ChangeNormalization"; +import { ReceiptPayload, signReceipt } from "./Signing"; +import { + checkAgentHourlyLimit, + checkAgentPendingLimit, + clampExpirySeconds, +} from "./Limits"; +import { getRequestsEnabled } from "./DataManager"; +import { createNotification } from "./Notifications"; +import { publishToUser } from "./WsHub"; +import { createLogger } from "./logger"; + +const log = createLogger("CHANGE_REQ"); + +// ── Result helpers ────────────────────────────────────────────────────────────── + +export type ServiceOk = { ok: true; data: T }; +export type ServiceErr = { ok: false; status: number; message: string }; +export type ServiceResult = ServiceOk | ServiceErr; + +const ok = (data: T): ServiceOk => ({ ok: true, data }); +const err = (status: number, message: string): ServiceErr => ({ ok: false, status, message }); + +// ── Serialization ──────────────────────────────────────────────────────────────── + +function approvalUrl(publicId: string): string { + return `${env.UI_URL.replace(/\/+$/, "")}/requests/${publicId}`; +} + +/** The platform-signed receipt for a decided request (APPROVED/REJECTED/CONSUMED). */ +export function buildReceipt(request: ChangeRequest) { + const decidedState = + request.state === "CONSUMED" ? "APPROVED" : (request.state as string); + if ( + (decidedState !== "APPROVED" && decidedState !== "REJECTED") || + !request.signature || + !request.decidedAt || + request.approverId === null + ) { + return null; + } + const payload: ReceiptPayload = { + request_id: request.publicId, + decision: decidedState as "APPROVED" | "REJECTED", + content_hash: request.contentHash, + approver_id: request.approverId, + decided_at: request.decidedAt.toISOString(), + }; + return { + payload, + signature: request.signature, + algorithm: "HMAC-SHA256", + consumed: request.state === "CONSUMED", + consumed_at: request.consumedAt ? request.consumedAt.toISOString() : null, + }; +} + +export function serializeRequest(request: ChangeRequest & { agent?: Agent }) { + const receipt = buildReceipt(request); + return { + request_id: request.publicId, + title: request.title, + description: request.description, + changes: request.changes, + metadata: request.metadata ?? null, + content_hash: request.contentHash, + state: request.state, + comment: request.comment, + expires_at: request.expiresAt.toISOString(), + created_at: request.createdAt.toISOString(), + updated_at: request.updatedAt.toISOString(), + decided_at: request.decidedAt ? request.decidedAt.toISOString() : null, + consumed_at: request.consumedAt ? request.consumedAt.toISOString() : null, + cancelled_at: request.cancelledAt ? request.cancelledAt.toISOString() : null, + update_count: request.updateCount, + resubmitted: request.resubmitted, + approval_url: approvalUrl(request.publicId), + receipt, + }; +} + +// ── Expiry materialization ────────────────────────────────────────────────────── + +/** + * Lazily transition an open (PENDING / CHANGES_REQUESTED) request to EXPIRED when + * it is read past its expiry. Keeps reads correct even between cron sweeps. + */ +export async function materializeExpiry(request: T): Promise { + const open = request.state === "PENDING" || request.state === "CHANGES_REQUESTED"; + if (open && request.expiresAt.getTime() <= Date.now()) { + const updated = await prisma.changeRequest.update({ + where: { id: request.id }, + data: { state: "EXPIRED" }, + }); + return { ...request, ...updated }; + } + return request; +} + +// ── Agent actions ──────────────────────────────────────────────────────────────── + +export type CreateRequestInput = { + title: string; + description?: string | null; + changes: Change[]; + expires_in?: number | null; + metadata?: Record | null; +}; + +export async function createChangeRequest( + agent: Agent, + input: CreateRequestInput, +): Promise>> { + if (!(await getRequestsEnabled())) { + return err(403, "Request submission is currently disabled by the administrator"); + } + + if (!input.title || input.title.trim().length === 0) { + return err(400, "title is required"); + } + if (!Array.isArray(input.changes) || input.changes.length === 0) { + return err(400, "changes must be a non-empty array"); + } + + const hourly = await checkAgentHourlyLimit(agent.id); + if (!hourly.allowed) return err(429, hourly.reason); + + const pending = await checkAgentPendingLimit(agent.id, agent.maxPendingRequests); + if (!pending.allowed) return err(429, pending.reason); + + const normalized = normalizeChanges(input.changes); + const contentHash = computeContentHash({ + title: input.title, + description: input.description ?? null, + changes: input.changes, + }); + const expiresInSeconds = clampExpirySeconds(input.expires_in ?? undefined); + const expiresAt = new Date(Date.now() + expiresInSeconds * 1000); + + const created = await prisma.changeRequest.create({ + data: { + title: input.title.trim(), + description: input.description?.trim() || null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + changes: normalized as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rawChanges: input.changes as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata: (input.metadata ?? undefined) as any, + contentHash, + expiresAt, + state: "PENDING", + agentId: agent.id, + userId: agent.ownerId, + }, + }); + + await createNotification({ + userId: agent.ownerId, + type: "REQUEST_CREATED", + title: "New change request", + message: `${agent.name} submitted "${created.title}" for review`, + requestPublicId: created.publicId, + }); + + log.info({ requestId: created.publicId, agentId: agent.id }, "Change request created"); + return ok(serializeRequest(created)); +} + +export async function updateChangeRequest( + agent: Agent, + publicId: string, + input: CreateRequestInput, +): Promise>> { + const existing = await prisma.changeRequest.findUnique({ where: { publicId } }); + if (!existing || existing.agentId !== agent.id) { + return err(404, "Change request not found"); + } + const request = await materializeExpiry(existing); + + // Updates are only possible before a final decision. + if (request.state !== "CHANGES_REQUESTED" && request.state !== "PENDING") { + return err( + 409, + `Cannot update a request in state ${request.state}; only PENDING or CHANGES_REQUESTED requests may be updated`, + ); + } + + if (!input.title || input.title.trim().length === 0) { + return err(400, "title is required"); + } + if (!Array.isArray(input.changes) || input.changes.length === 0) { + return err(400, "changes must be a non-empty array"); + } + + const normalized = normalizeChanges(input.changes); + const contentHash = computeContentHash({ + title: input.title, + description: input.description ?? null, + changes: input.changes, + }); + + const updated = await prisma.changeRequest.update({ + where: { id: request.id }, + data: { + title: input.title.trim(), + description: input.description?.trim() || null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + changes: normalized as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rawChanges: input.changes as any, + ...(input.metadata !== undefined + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + { metadata: (input.metadata ?? undefined) as any } + : {}), + contentHash, + state: "PENDING", + resubmitted: true, + updateCount: { increment: 1 }, + lastAgentUpdateAt: new Date(), + // A fresh decision must be made on the new content. + decidedAt: null, + approverId: null, + signature: null, + receiptIssuedAt: null, + }, + }); + + await createNotification({ + userId: request.userId, + type: "REQUEST_UPDATED", + title: "Change request updated", + message: `${agent.name} updated "${updated.title}" after your requested changes`, + requestPublicId: updated.publicId, + }); + + log.info({ requestId: updated.publicId, agentId: agent.id }, "Change request updated"); + return ok(serializeRequest(updated)); +} + +export async function getChangeRequestForAgent( + agent: Agent, + publicId: string, +): Promise>> { + const existing = await prisma.changeRequest.findUnique({ where: { publicId } }); + if (!existing || existing.agentId !== agent.id) { + return err(404, "Change request not found"); + } + const request = await materializeExpiry(existing); + return ok(serializeRequest(request)); +} + +export async function cancelChangeRequest( + agent: Agent, + publicId: string, +): Promise>> { + const existing = await prisma.changeRequest.findUnique({ where: { publicId } }); + if (!existing || existing.agentId !== agent.id) { + return err(404, "Change request not found"); + } + const request = await materializeExpiry(existing); + + if (request.state !== "PENDING" && request.state !== "CHANGES_REQUESTED") { + return err(409, `Cannot cancel a request in state ${request.state}`); + } + + const updated = await prisma.changeRequest.update({ + where: { id: request.id }, + data: { state: "CANCELLED", cancelledAt: new Date() }, + }); + + await createNotification({ + userId: request.userId, + type: "REQUEST_CANCELLED", + title: "Change request cancelled", + message: `${agent.name} cancelled "${updated.title}"`, + requestPublicId: updated.publicId, + }); + + return ok(serializeRequest(updated)); +} + +export async function consumeApproval( + agent: Agent, + publicId: string, +): Promise>> { + const existing = await prisma.changeRequest.findUnique({ where: { publicId } }); + if (!existing || existing.agentId !== agent.id) { + return err(404, "Change request not found"); + } + const request = await materializeExpiry(existing); + + if (request.state === "CONSUMED") { + return err(409, "This approval has already been consumed"); + } + if (request.state !== "APPROVED") { + return err(409, `Only APPROVED requests can be consumed (current state: ${request.state})`); + } + + const updated = await prisma.changeRequest.update({ + where: { id: request.id }, + data: { state: "CONSUMED", consumedAt: new Date() }, + }); + + await createNotification({ + userId: request.userId, + type: "REQUEST_CONSUMED", + title: "Approval consumed", + message: `${agent.name} consumed the approval for "${updated.title}" and is proceeding`, + requestPublicId: updated.publicId, + }); + + return ok(serializeRequest(updated)); +} + +export async function listChangeRequestsForAgent( + agent: Agent, + opts: { state?: string; page?: number; pageSize?: number }, +) { + const page = Math.max(1, opts.page ?? 1); + const pageSize = Math.min(100, Math.max(1, opts.pageSize ?? 20)); + const where = { + agentId: agent.id, + ...(opts.state ? { state: opts.state as ChangeRequest["state"] } : {}), + }; + const [total, rows] = await Promise.all([ + prisma.changeRequest.count({ where }), + prisma.changeRequest.findMany({ + where, + orderBy: { createdAt: "desc" }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + ]); + return { + page, + pageSize, + total, + requests: rows.map((r) => serializeRequest(r)), + }; +} + +// ── Human decision ──────────────────────────────────────────────────────────────── + +export type DecisionKind = "APPROVE" | "REJECT" | "REQUEST_CHANGES"; + +export async function decideChangeRequest( + user: User, + publicId: string, + decision: DecisionKind, + comment?: string | null, +): Promise>> { + const existing = await prisma.changeRequest.findUnique({ where: { publicId } }); + if (!existing || existing.userId !== user.id) { + return err(404, "Change request not found"); + } + const request = await materializeExpiry(existing); + + if (request.state !== "PENDING") { + return err( + 409, + `Only PENDING requests can be decided (current state: ${request.state})`, + ); + } + + const trimmedComment = comment?.trim() || null; + if (trimmedComment && trimmedComment.length > 500) { + return err(400, "Comment must be 500 characters or fewer"); + } + + const decidedAt = new Date(); + + if (decision === "REQUEST_CHANGES") { + if (!trimmedComment) { + return err(400, "A comment is required when requesting changes"); + } + const updated = await prisma.changeRequest.update({ + where: { id: request.id }, + data: { + state: "CHANGES_REQUESTED", + comment: trimmedComment, + decidedAt, + approverId: user.id, + resubmitted: false, + }, + }); + await notifyOwnerDecision(updated, "CHANGES_REQUESTED"); + return ok(serializeRequest(updated)); + } + + // APPROVE / REJECT — sign the platform receipt binding decision to content. + const receiptDecision = decision === "APPROVE" ? "APPROVED" : "REJECTED"; + const signature = signReceipt({ + request_id: request.publicId, + decision: receiptDecision, + content_hash: request.contentHash, + approver_id: user.id, + decided_at: decidedAt.toISOString(), + }); + + const updated = await prisma.changeRequest.update({ + where: { id: request.id }, + data: { + state: receiptDecision, + comment: trimmedComment, + decidedAt, + approverId: user.id, + signature, + receiptIssuedAt: decidedAt, + }, + }); + + await notifyOwnerDecision(updated, receiptDecision); + return ok(serializeRequest(updated)); +} + +async function notifyOwnerDecision(request: ChangeRequest, decision: string) { + const map: Record< + string, + { type: Parameters[0]["type"]; title: string; verb: string } + > = { + APPROVED: { type: "REQUEST_APPROVED", title: "Request approved", verb: "approved" }, + REJECTED: { type: "REQUEST_REJECTED", title: "Request rejected", verb: "rejected" }, + CHANGES_REQUESTED: { + type: "CHANGES_REQUESTED", + title: "Changes requested", + verb: "requested changes on", + }, + }; + const entry = map[decision]; + if (!entry) return; + await createNotification({ + userId: request.userId, + type: entry.type, + title: entry.title, + message: `You ${entry.verb} "${request.title}"`, + requestPublicId: request.publicId, + }); + // Nudge all of the human's open tabs to refresh this request. + await publishToUser(request.userId, { + kind: "request_event", + event: decision, + requestPublicId: request.publicId, + }); +} diff --git a/Backend/src/lib/DataManager.ts b/Backend/src/lib/DataManager.ts new file mode 100644 index 0000000..784b74f --- /dev/null +++ b/Backend/src/lib/DataManager.ts @@ -0,0 +1,42 @@ +import { GlobalSettingType } from "@prisma/client"; +import { prisma } from "./db"; + +async function getSetting(type: GlobalSettingType): Promise { + const row = await prisma.globalSetting.findUnique({ where: { type } }); + return row?.value ?? null; +} + +async function setSetting(type: GlobalSettingType, value: string): Promise { + await prisma.globalSetting.upsert({ + where: { type }, + create: { type, value }, + update: { value }, + }); +} + +export async function getRegistrationEnabled(): Promise { + const val = await getSetting("registration_enabled"); + if (val !== null) return val === "true"; + return true; // registration is enabled by default +} + +export async function setRegistrationEnabled(enabled: boolean): Promise { + await setSetting("registration_enabled", String(enabled)); +} + +export async function getRequestsEnabled(): Promise { + const val = await getSetting("requests_enabled"); + if (val !== null) return val === "true"; + return true; // request submission is enabled by default +} + +export async function setRequestsEnabled(enabled: boolean): Promise { + await setSetting("requests_enabled", String(enabled)); +} + +export async function getGlobalSettings() { + return { + registration_enabled: await getRegistrationEnabled(), + requests_enabled: await getRequestsEnabled(), + }; +} diff --git a/Backend/src/lib/IconValidation.ts b/Backend/src/lib/IconValidation.ts new file mode 100644 index 0000000..c622b28 --- /dev/null +++ b/Backend/src/lib/IconValidation.ts @@ -0,0 +1,79 @@ +const MAX_ICON_BYTES = 1024 * 1024; // 1 MB +const SUPPORTED_MIME = new Set(["image/jpeg", "image/png", "image/gif"]); +const FETCH_TIMEOUT_MS = 8000; + +export type IconValidationResult = + | { valid: true; mime: string; bytes: number } + | { valid: false; reason: string }; + +/** + * Verify that an icon URL points to a real, supported, appropriately-sized image. + * The image is fetched ONCE and NOT cached/persisted — only validated. Formats + * other than jpg/png/gif and images over 1 MB are rejected. + */ +export async function validateIconUrl(url: string): Promise { + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return { valid: false, reason: "Icon URL is not a valid URL" }; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return { valid: false, reason: "Icon URL must use http or https" }; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + let response: Response; + try { + response = await fetch(parsed.toString(), { + method: "GET", + redirect: "follow", + signal: controller.signal, + headers: { Accept: "image/*" }, + }); + } catch { + clearTimeout(timeout); + return { valid: false, reason: "Could not fetch the icon URL" }; + } + clearTimeout(timeout); + + if (!response.ok) { + return { valid: false, reason: `Icon URL returned HTTP ${response.status}` }; + } + + // Reject early if the server advertises an oversized body. + const contentLength = response.headers.get("content-length"); + if (contentLength && Number(contentLength) > MAX_ICON_BYTES) { + return { valid: false, reason: "Icon image exceeds the 1 MB limit" }; + } + + let arrayBuffer: ArrayBuffer; + try { + arrayBuffer = await response.arrayBuffer(); + } catch { + return { valid: false, reason: "Failed to read the icon image data" }; + } + + const buffer = Buffer.from(arrayBuffer); + if (buffer.byteLength > MAX_ICON_BYTES) { + return { valid: false, reason: "Icon image exceeds the 1 MB limit" }; + } + if (buffer.byteLength === 0) { + return { valid: false, reason: "Icon URL returned an empty body" }; + } + + // Detect the true format from the bytes, not the advertised content-type. + // file-type is ESM-only; use a dynamic import from this CommonJS build. + const { fileTypeFromBuffer } = await import("file-type"); + const detected = await fileTypeFromBuffer(buffer); + if (!detected || !SUPPORTED_MIME.has(detected.mime)) { + return { + valid: false, + reason: "Icon must be a JPG, PNG, or GIF image", + }; + } + + return { valid: true, mime: detected.mime, bytes: buffer.byteLength }; +} diff --git a/Backend/src/lib/Limits.ts b/Backend/src/lib/Limits.ts new file mode 100644 index 0000000..50b261f --- /dev/null +++ b/Backend/src/lib/Limits.ts @@ -0,0 +1,90 @@ +import { prisma } from "./db"; + +// ── Platform limits ──────────────────────────────────────────────────────────── + +/** Max change requests an agent may create per rolling hour. */ +export const MAX_REQUESTS_PER_HOUR = 15; + +/** Max agents a single human account may own. */ +export const MAX_AGENTS_PER_USER = 5; + +/** Pending-request cap bounds (human-configurable per agent). */ +export const MIN_PENDING_LIMIT = 1; +export const MAX_PENDING_LIMIT = 10; +export const DEFAULT_PENDING_LIMIT = 5; + +/** Request expiry bounds, in seconds. */ +export const DEFAULT_EXPIRY_SECONDS = 30 * 60; // 30 minutes +export const MIN_EXPIRY_SECONDS = 60; // 1 minute +export const MAX_EXPIRY_SECONDS = 12 * 60 * 60; // 12 hours + +/** Auto-delete retention bounds, in days. */ +export const MIN_AUTO_DELETE_DAYS = 7; +export const DEFAULT_AUTO_DELETE_DAYS = 30; + +// ── Pure clamps ──────────────────────────────────────────────────────────────── + +export function clampPendingLimit(value: number): number { + if (!Number.isFinite(value)) return DEFAULT_PENDING_LIMIT; + return Math.min(MAX_PENDING_LIMIT, Math.max(MIN_PENDING_LIMIT, Math.trunc(value))); +} + +export function clampExpirySeconds(value: number | undefined | null): number { + if (value === undefined || value === null || !Number.isFinite(value)) { + return DEFAULT_EXPIRY_SECONDS; + } + return Math.min(MAX_EXPIRY_SECONDS, Math.max(MIN_EXPIRY_SECONDS, Math.trunc(value))); +} + +export function clampAutoDeleteDays(value: number): number { + if (!Number.isFinite(value)) return DEFAULT_AUTO_DELETE_DAYS; + return Math.max(MIN_AUTO_DELETE_DAYS, Math.trunc(value)); +} + +// ── Limit checks (DB-backed) ──────────────────────────────────────────────────── + +export type LimitResult = { allowed: true } | { allowed: false; reason: string }; + +/** An agent may create at most MAX_REQUESTS_PER_HOUR requests per rolling hour. */ +export async function checkAgentHourlyLimit(agentId: number): Promise { + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + const count = await prisma.changeRequest.count({ + where: { agentId, createdAt: { gte: oneHourAgo } }, + }); + if (count >= MAX_REQUESTS_PER_HOUR) { + return { + allowed: false, + reason: `Rate limit reached: an agent may create at most ${MAX_REQUESTS_PER_HOUR} requests per hour`, + }; + } + return { allowed: true }; +} + +/** An agent may have at most `maxPendingRequests` requests in PENDING state. */ +export async function checkAgentPendingLimit( + agentId: number, + maxPendingRequests: number, +): Promise { + const count = await prisma.changeRequest.count({ + where: { agentId, state: "PENDING" }, + }); + if (count >= maxPendingRequests) { + return { + allowed: false, + reason: `Pending limit reached: this agent may have at most ${maxPendingRequests} pending requests at a time`, + }; + } + return { allowed: true }; +} + +/** A human may own at most MAX_AGENTS_PER_USER agents. */ +export async function checkAgentCountLimit(userId: number): Promise { + const count = await prisma.agent.count({ where: { ownerId: userId } }); + if (count >= MAX_AGENTS_PER_USER) { + return { + allowed: false, + reason: `Agent limit reached: a human may own at most ${MAX_AGENTS_PER_USER} agents`, + }; + } + return { allowed: true }; +} diff --git a/Backend/src/lib/Notifications.ts b/Backend/src/lib/Notifications.ts new file mode 100644 index 0000000..43a8e02 --- /dev/null +++ b/Backend/src/lib/Notifications.ts @@ -0,0 +1,46 @@ +import { NotificationType } from "@prisma/client"; +import { prisma } from "./db"; +import { publishToUser } from "./WsHub"; +import { createLogger } from "./logger"; + +const log = createLogger("NOTIFY"); + +export type CreateNotificationInput = { + userId: number; + type: NotificationType; + title: string; + message: string; + requestPublicId?: string | null; +}; + +/** + * Persist a notification and push it to the user's live websocket channel. + * Notifications always persist (retrievable via REST) — the websocket delivery + * is best-effort on top of that. + */ +export async function createNotification(input: CreateNotificationInput) { + const notification = await prisma.notification.create({ + data: { + userId: input.userId, + type: input.type, + title: input.title, + message: input.message, + requestPublicId: input.requestPublicId ?? null, + }, + }); + + try { + const unreadCount = await prisma.notification.count({ + where: { userId: input.userId, read: false }, + }); + await publishToUser(input.userId, { + kind: "notification", + notification, + unreadCount, + }); + } catch (err) { + log.warn({ err, userId: input.userId }, "Failed to push realtime notification"); + } + + return notification; +} diff --git a/Backend/src/lib/RouteAuth.ts b/Backend/src/lib/RouteAuth.ts new file mode 100644 index 0000000..18fcc01 --- /dev/null +++ b/Backend/src/lib/RouteAuth.ts @@ -0,0 +1,43 @@ +import type { Agent, Session, User } from "@prisma/client"; +import type { AuthState } from "./Authentication"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function getAuth(ctr: any): AuthState { + if (typeof ctr.getAuth === "function") return ctr.getAuth(); + return { success: false, message: "Auth not resolved", method: "none" }; +} + +/** Require a logged-in human. Returns the user/session or null (after replying 401). */ +export function requireSession(ctr: any): { user: User; session: Session } | null { + const auth = getAuth(ctr); + if (!auth.success || auth.method !== "session") { + ctr.status(401).print({ status: "FAILED", message: "You must be logged in" }); + return null; + } + return { user: auth.user, session: auth.session }; +} + +/** Require a valid agent API key. Returns the agent/owner or null (after replying 401). */ +export function requireAgent(ctr: any): { agent: Agent; owner: User } | null { + const auth = getAuth(ctr); + if (!auth.success || auth.method !== "agent") { + ctr.status(401).print({ + status: "FAILED", + message: "A valid agent API key is required (x-api-key header)", + }); + return null; + } + return { agent: auth.agent, owner: auth.owner }; +} + +/** Require an admin human. Returns the user or null (after replying 401/403). */ +export function requireAdmin(ctr: any): { user: User } | null { + const session = requireSession(ctr); + if (!session) return null; + if (session.user.role !== "ADMIN") { + ctr.status(403).print({ status: "FAILED", message: "Administrator access required" }); + return null; + } + return { user: session.user }; +} diff --git a/Backend/src/lib/Signing.ts b/Backend/src/lib/Signing.ts new file mode 100644 index 0000000..bd29e41 --- /dev/null +++ b/Backend/src/lib/Signing.ts @@ -0,0 +1,41 @@ +import { createHmac, timingSafeEqual } from "crypto"; +import { env } from "./env"; + +/** + * The canonical, platform-signed decision receipt. Agents verify this through the + * API before applying changes: the signature is an HMAC-SHA256 over the canonical + * receipt fields keyed by INSTANCE_SECRET, so a decision cannot be forged or + * replayed against a different request or different content. + */ +export type ReceiptPayload = { + request_id: string; + decision: "APPROVED" | "REJECTED"; + content_hash: string; + approver_id: number; + decided_at: string; // ISO 8601, UTC +}; + +function canonicalReceipt(p: ReceiptPayload): string { + // Fixed field order — never reorder, the signature depends on it. + return [ + `request_id=${p.request_id}`, + `decision=${p.decision}`, + `content_hash=${p.content_hash}`, + `approver_id=${p.approver_id}`, + `decided_at=${p.decided_at}`, + ].join("\n"); +} + +export function signReceipt(payload: ReceiptPayload): string { + return createHmac("sha256", env.INSTANCE_SECRET) + .update(canonicalReceipt(payload)) + .digest("hex"); +} + +export function verifyReceipt(payload: ReceiptPayload, signature: string): boolean { + const expected = signReceipt(payload); + const a = Buffer.from(expected, "hex"); + const b = Buffer.from(signature, "hex"); + if (a.length !== b.length) return false; + return timingSafeEqual(a, b); +} diff --git a/Backend/src/lib/SystemCrons.ts b/Backend/src/lib/SystemCrons.ts new file mode 100644 index 0000000..f7757a4 --- /dev/null +++ b/Backend/src/lib/SystemCrons.ts @@ -0,0 +1,123 @@ +import type { PrismaClient } from "@prisma/client"; +import { createLogger } from "./logger"; +import { createNotification } from "./Notifications"; + +const log = createLogger("SYSTEM_CRONS"); + +type SystemCronDependencies = { prisma: PrismaClient }; + +type ScheduledJob = { + name: string; + intervalMs: number; + runOnStart?: boolean; + run: () => Promise; +}; + +type SystemCronHandle = { stop: () => void }; + +function scheduleJob(job: ScheduledJob): SystemCronHandle { + let stopped = false; + let timer: ReturnType | null = null; + + const tick = async () => { + if (stopped) return; + try { + await job.run(); + } catch (error) { + log.error({ err: error, job: job.name }, "System cron failed"); + } + if (!stopped) { + timer = setTimeout(() => void tick(), job.intervalMs); + } + }; + + if (job.runOnStart) void tick(); + else timer = setTimeout(() => void tick(), job.intervalMs); + + return { + stop: () => { + stopped = true; + if (timer !== null) clearTimeout(timer); + }, + }; +} + +export function startSystemCrons(deps: SystemCronDependencies): SystemCronHandle { + const jobs: ScheduledJob[] = [ + { + name: "expire-requests", + intervalMs: 30 * 1000, + runOnStart: true, + run: async () => { + await expireRequests(deps); + }, + }, + { + name: "auto-delete-requests", + intervalMs: 60 * 60 * 1000, // hourly + runOnStart: true, + run: async () => { + await autoDeleteRequests(deps); + }, + }, + ]; + const handles = jobs.map(scheduleJob); + return { stop: () => handles.forEach((h) => h.stop()) }; +} + +/** Transition open requests past their expiry to EXPIRED and notify their owners. */ +export async function expireRequests({ prisma }: SystemCronDependencies): Promise { + const now = new Date(); + const expired = await prisma.changeRequest.findMany({ + where: { + state: { in: ["PENDING", "CHANGES_REQUESTED"] }, + expiresAt: { lt: now }, + }, + select: { id: true, publicId: true, title: true, userId: true }, + }); + if (expired.length === 0) return 0; + + await prisma.changeRequest.updateMany({ + where: { id: { in: expired.map((r) => r.id) } }, + data: { state: "EXPIRED" }, + }); + + for (const r of expired) { + await createNotification({ + userId: r.userId, + type: "REQUEST_EXPIRED", + title: "Change request expired", + message: `"${r.title}" expired before it was reviewed`, + requestPublicId: r.publicId, + }); + } + + log.info({ count: expired.length }, "Expired stale change requests"); + return expired.length; +} + +/** + * Delete change requests older than each opted-in user's retention window. + * Disabled by default per user; retention is clamped to a 7-day minimum. + */ +export async function autoDeleteRequests({ prisma }: SystemCronDependencies): Promise { + const users = await prisma.user.findMany({ + where: { autoDeleteEnabled: true }, + select: { id: true, autoDeleteDays: true }, + }); + if (users.length === 0) return 0; + + let totalDeleted = 0; + for (const user of users) { + const days = Math.max(7, user.autoDeleteDays); + const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + const result = await prisma.changeRequest.deleteMany({ + where: { userId: user.id, createdAt: { lt: cutoff } }, + }); + totalDeleted += result.count; + } + if (totalDeleted > 0) { + log.info({ count: totalDeleted }, "Auto-deleted old change requests"); + } + return totalDeleted; +} diff --git a/Backend/src/lib/WsHub.ts b/Backend/src/lib/WsHub.ts new file mode 100644 index 0000000..c8b4f96 --- /dev/null +++ b/Backend/src/lib/WsHub.ts @@ -0,0 +1,30 @@ +import { Channel } from "rjweb-server"; + +/** + * Real-time hub. One RJWEB Channel per human account; the websocket endpoint + * subscribes each authenticated socket to its owner's channel via + * `ctr.printChannel(getUserChannel(userId))`. Any server-side event for that + * user (new notification, request state change) is published to the channel and + * fanned out to every open socket. + */ +const userChannels = new Map>(); + +export function getUserChannel(userId: number): Channel { + let channel = userChannels.get(userId); + if (!channel) { + channel = new Channel(); + userChannels.set(userId, channel); + } + return channel; +} + +export type RealtimeEvent = + | { kind: "notification"; notification: unknown; unreadCount: number } + | { kind: "request_event"; event: string; requestPublicId: string } + | { kind: "ping"; at: string }; + +export async function publishToUser(userId: number, event: RealtimeEvent): Promise { + const channel = userChannels.get(userId); + if (!channel) return; // nobody connected — nothing to push + await channel.send("text", JSON.stringify(event)); +} diff --git a/Backend/src/lib/db.ts b/Backend/src/lib/db.ts new file mode 100644 index 0000000..58d1d6f --- /dev/null +++ b/Backend/src/lib/db.ts @@ -0,0 +1,12 @@ +import { env } from "./env"; +import { PrismaClient } from "@prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; + +const adapter = new PrismaPg({ connectionString: env.DATABASE_URL }); + +export const prisma = new PrismaClient({ + adapter, + log: ["info", "error", "warn"], + errorFormat: "pretty", + transactionOptions: { timeout: 30000, maxWait: 20000 }, +}); diff --git a/Backend/src/lib/env.ts b/Backend/src/lib/env.ts new file mode 100644 index 0000000..4252519 --- /dev/null +++ b/Backend/src/lib/env.ts @@ -0,0 +1,51 @@ +import dotenv from "dotenv"; +import { join } from "path"; +import { z } from "zod"; + +if (process.env.NODE_ENV !== "test") { + dotenv.config({ path: join(__dirname, "../../.env") }); +} + +const baseSchema = z.object({ + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + DATABASE_URL: z + .string() + .default("postgresql://patchpass:patchpass@localhost:5433/patchpass"), + PORT: z.coerce.number().int().positive(), + UI_URL: z.string().min(1), + REACT_APP_API_URL: z.string().min(1), + DOMAIN: z.string().min(1), + CORS_URLS: z.string().min(1), + INSTANCE_SECRET: z.string().default("default_insecure_secret_please_set"), + RATELIMIT: z.coerce.number().int().nonnegative().default(0), + LOG_LEVEL: z.string().default("info"), + REQUEST_DEBUGGING: z + .enum(["true", "false"]) + .transform((v) => v === "true") + .default(false), + RESPONSE_DEBUGGING: z + .enum(["true", "false"]) + .transform((v) => v === "true") + .default(false), +}); + +// In test mode the server never starts, so production-required vars get safe defaults. +const testSchema = baseSchema.extend({ + PORT: z.coerce.number().int().positive().default(3000), + UI_URL: z.string().default("http://localhost:3000"), + REACT_APP_API_URL: z.string().default("http://localhost:3000"), + DOMAIN: z.string().default("localhost"), + CORS_URLS: z.string().default("http://localhost:3000"), +}); + +const isTest = process.env.NODE_ENV === "test"; +const result = (isTest ? testSchema : baseSchema).safeParse(process.env); + +if (!result.success) { + const formatted = result.error.issues + .map((i) => ` ${i.path.join(".")}: ${i.message}`) + .join("\n"); + throw new Error(`Invalid environment variables:\n${formatted}`); +} + +export const env = result.data; diff --git a/Backend/src/lib/errors.ts b/Backend/src/lib/errors.ts new file mode 100644 index 0000000..f9a66cc --- /dev/null +++ b/Backend/src/lib/errors.ts @@ -0,0 +1,9 @@ +export const ERROR_MESSAGES = { + UNAUTHORIZED: { code: 401, message: "You are not authorized to access this resource." }, + FORBIDDEN: { code: 403, message: "You do not have permission to access this resource." }, + NOT_FOUND: { code: 404, message: "The requested resource was not found." }, + INTERNAL_SERVER_ERROR: { code: 500, message: "An unexpected server error has occurred." }, + BAD_REQUEST: { code: 400, message: "The request was invalid or malformed." }, + CONFLICT: { code: 409, message: "The request conflicts with the current state of the resource." }, + TOO_MANY_REQUESTS: { code: 429, message: "Too many requests. Please slow down." }, +} as const; diff --git a/Backend/src/lib/logger.ts b/Backend/src/lib/logger.ts new file mode 100644 index 0000000..706ee91 --- /dev/null +++ b/Backend/src/lib/logger.ts @@ -0,0 +1,14 @@ +import { env } from "./env"; +import pino from "pino"; + +export const logger = pino({ + level: env.LOG_LEVEL, + transport: + env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, +}); + +export function createLogger(component: string) { + return logger.child({ component }); +} diff --git a/Backend/src/lib/mcp/mcp.ts b/Backend/src/lib/mcp/mcp.ts new file mode 100644 index 0000000..96ac98c --- /dev/null +++ b/Backend/src/lib/mcp/mcp.ts @@ -0,0 +1,68 @@ +import type { Agent } from "@prisma/client"; +import { tools, toolMap } from "./tools"; + +type JsonRpcId = string | number | null | undefined; + +function ok(id: JsonRpcId, result: unknown) { + return { status: 200, body: { jsonrpc: "2.0" as const, id: id ?? null, result } }; +} + +function rpcErr(id: JsonRpcId, code: number, message: string) { + return { status: 200, body: { jsonrpc: "2.0" as const, id: id ?? null, error: { code, message } } }; +} + +export type McpRequest = { + jsonrpc: "2.0"; + id?: JsonRpcId; + method: string; + params?: unknown; +}; + +export async function handleMcpRequest( + req: McpRequest, + ctx: { agent: Agent }, +): Promise<{ status: number; body: unknown }> { + const { id, method, params } = req; + + // Notifications — acknowledge with no body. + if (method.startsWith("notifications/") || method === "initialized") { + return { status: 202, body: {} }; + } + + switch (method) { + case "initialize": + return ok(id, { + protocolVersion: "2024-11-05", + capabilities: { tools: { listChanged: false } }, + serverInfo: { name: "patchpass", version: "1.0.0" }, + instructions: + "PatchPass is a human approval layer. Call get_docs first. Before taking consequential actions, call create_request and wait for approval, then consume_approval before proceeding.", + }); + + case "ping": + return ok(id, {}); + + case "tools/list": + return ok(id, { + tools: tools.map(({ name, description, inputSchema }) => ({ + name, + description, + inputSchema, + })), + }); + + case "tools/call": { + const p = params as { name?: string; arguments?: Record } | undefined; + const toolName = p?.name; + const args = (p?.arguments ?? {}) as Record; + if (!toolName) return rpcErr(id, -32602, "Invalid params: missing name"); + const tool = toolMap.get(toolName); + if (!tool) return rpcErr(id, -32601, `Unknown tool: ${toolName}`); + const result = await tool.handler(args, ctx); + return ok(id, result); + } + + default: + return rpcErr(id, -32601, `Method not found: ${method}`); + } +} diff --git a/Backend/src/lib/mcp/shared.ts b/Backend/src/lib/mcp/shared.ts new file mode 100644 index 0000000..c662cdc --- /dev/null +++ b/Backend/src/lib/mcp/shared.ts @@ -0,0 +1,74 @@ +import type { Agent } from "@prisma/client"; + +export type McpToolResult = { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +}; + +export type ToolContext = { + agent: Agent; +}; + +export type ToolHandler = ( + args: Record, + ctx: ToolContext, +) => Promise; + +export type McpToolDef = { + name: string; + description: string; + inputSchema: Record; + handler: ToolHandler; +}; + +export function text(s: string): McpToolResult { + return { content: [{ type: "text", text: s }] }; +} + +export function json(v: unknown): McpToolResult { + return text(JSON.stringify(v, null, 2)); +} + +export function errResult(message: string): McpToolResult { + return { content: [{ type: "text", text: message }], isError: true }; +} + +/** Bridge a ChangeRequestService result to an MCP tool result. */ +export function fromService(result: { + ok: boolean; + status?: number; + message?: string; + data?: unknown; +}): McpToolResult { + if (!result.ok) return errResult(result.message ?? "Request failed"); + return json(result.data); +} + +// Shared JSON-schema fragment for the `changes` array so every tool documents it +// identically. Agents are told to use correct types so humans see rich diffs. +export const CHANGES_SCHEMA = { + type: "array", + minItems: 1, + description: + "List of proposed changes. Each item is one of three types: unified_diff (git-style diff applied with `patch -p1`), config (a keyed before/after value with a content_type), or custom (an arbitrary labelled before/after).", + items: { + type: "object", + properties: { + type: { type: "string", enum: ["unified_diff", "config", "custom"] }, + path: { + type: "string", + description: "File path (unified_diff) or config key path (config)", + }, + content: { type: "string", description: "The unified diff text (unified_diff only)" }, + label: { type: "string", description: "Human-readable label (custom only)" }, + before: { description: "Value before the change (config/custom)" }, + after: { description: "Value after the change (config/custom)" }, + content_type: { + type: "string", + description: + "Type hint for config values, e.g. 'integer', 'string', 'boolean' — so the human sees 30 -> 60, not \"30\" -> \"60\"", + }, + }, + required: ["type"], + }, +}; diff --git a/Backend/src/lib/mcp/tools/docs.ts b/Backend/src/lib/mcp/tools/docs.ts new file mode 100644 index 0000000..3e3fbb2 --- /dev/null +++ b/Backend/src/lib/mcp/tools/docs.ts @@ -0,0 +1,46 @@ +import { McpToolDef, text } from "../shared"; + +const DOCS = `PatchPass — human approval layer for AI agents. + +WORKFLOW +1. Before taking any consequential action, call create_request with a clear title, + description, and a structured list of changes. You receive a request_id and an + approval_url. Share the approval_url with your human if helpful. +2. Poll get_request(request_id) until state is no longer PENDING. + - APPROVED → you may proceed. First call consume_approval(request_id). + - REJECTED → do NOT proceed. This is a hard blocker; a new request is required. + - CHANGES_REQUESTED → read the 'comment', revise, and call update_request. + - EXPIRED / CANCELLED → the request is dead; start over if still needed. +3. consume_approval(request_id) marks the approval as used (single-use) and returns + the platform-signed receipt. Only proceed after a successful consume. + +CHANGE TYPES +- unified_diff: { type, path, content } — a git-style diff (patch -p1). Preferred + for code/file edits. +- config: { type, path, before, after, content_type } — a keyed value change. Use the + correct content_type ('integer', 'boolean', 'string', ...) so the human sees + "30 -> 60" or "50% increase", not stringified values. +- custom: { type, label, before, after } — any other before/after pair. + +RECEIPTS +A decided request carries a 'receipt' with the decision, content_hash, approver_id, +decided_at, and an HMAC-SHA256 'signature'. The content_hash binds the decision to the +exact reviewed content: if you change anything after approval, the approval no longer +applies — submit a new request. + +LIMITS +- Max 15 requests per hour per agent. +- Limited simultaneous PENDING requests (configured by your human, default 5). +- Requests expire (default 30 min, up to 12 h via expires_in seconds).`; + +const tool: McpToolDef = { + name: "get_docs", + description: + "Read PatchPass conventions: the request/approval workflow, change types, receipts, and limits. Call this first.", + inputSchema: { type: "object", properties: {} }, + async handler() { + return text(DOCS); + }, +}; + +export default tool; diff --git a/Backend/src/lib/mcp/tools/index.ts b/Backend/src/lib/mcp/tools/index.ts new file mode 100644 index 0000000..4d8876f --- /dev/null +++ b/Backend/src/lib/mcp/tools/index.ts @@ -0,0 +1,24 @@ +import getDocs from "./docs"; +import { + cancelRequest, + consumeApprovalTool, + createRequest, + getRequest, + listRequests, + updateRequest, +} from "./requests"; +import { McpToolDef } from "../shared"; + +export type { McpToolResult, ToolContext, ToolHandler, McpToolDef } from "../shared"; + +export const tools: McpToolDef[] = [ + getDocs, + createRequest, + updateRequest, + getRequest, + listRequests, + cancelRequest, + consumeApprovalTool, +]; + +export const toolMap = new Map(tools.map((t) => [t.name, t])); diff --git a/Backend/src/lib/mcp/tools/requests.ts b/Backend/src/lib/mcp/tools/requests.ts new file mode 100644 index 0000000..2b7b7ba --- /dev/null +++ b/Backend/src/lib/mcp/tools/requests.ts @@ -0,0 +1,185 @@ +import { + cancelChangeRequest, + consumeApproval, + createChangeRequest, + getChangeRequestForAgent, + listChangeRequestsForAgent, + updateChangeRequest, + CreateRequestInput, +} from "../../ChangeRequestService"; +import { Change } from "../../ChangeNormalization"; +import { CHANGES_SCHEMA, McpToolDef, errResult, fromService, json } from "../shared"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function coerceChanges(raw: unknown): Change[] | { error: string } { + if (!Array.isArray(raw) || raw.length === 0) { + return { error: "changes must be a non-empty array" }; + } + const out: Change[] = []; + for (const [i, item] of raw.entries()) { + if (!item || typeof item !== "object") return { error: `changes[${i}] must be an object` }; + const c = item as any; + if (c.type === "unified_diff") { + if (typeof c.path !== "string" || typeof c.content !== "string") { + return { error: `changes[${i}] (unified_diff) needs string 'path' and 'content'` }; + } + out.push({ type: "unified_diff", path: c.path, content: c.content }); + } else if (c.type === "config") { + if (typeof c.path !== "string") return { error: `changes[${i}] (config) needs a string 'path'` }; + out.push({ + type: "config", + path: c.path, + before: c.before, + after: c.after, + content_type: typeof c.content_type === "string" ? c.content_type : undefined, + }); + } else if (c.type === "custom") { + if (typeof c.label !== "string") return { error: `changes[${i}] (custom) needs a string 'label'` }; + out.push({ type: "custom", label: c.label, before: c.before, after: c.after }); + } else { + return { error: `changes[${i}] has invalid type '${c.type}'` }; + } + } + return out; +} + +function buildInput(args: Record): CreateRequestInput | { error: string } { + const title = typeof args.title === "string" ? args.title : undefined; + if (!title) return { error: "title is required" }; + const changes = coerceChanges(args.changes); + if ("error" in changes) return { error: changes.error }; + return { + title, + description: typeof args.description === "string" ? args.description : null, + changes, + expires_in: typeof args.expires_in === "number" ? args.expires_in : undefined, + metadata: + args.metadata && typeof args.metadata === "object" + ? (args.metadata as Record) + : undefined, + }; +} + +const commonProps = { + title: { type: "string", description: "Short, human-readable title (1–200 chars)" }, + description: { type: "string", description: "Optional context for the reviewer" }, + changes: CHANGES_SCHEMA, + expires_in: { + type: "integer", + description: "Seconds until the request expires (60–43200; default 1800 = 30 min)", + }, + metadata: { + type: "object", + description: "Optional free-form context, e.g. { repository, environment }", + }, +}; + +export const createRequest: McpToolDef = { + name: "create_request", + description: + "Submit a change request for human approval. Returns request_id, approval_url, and state (PENDING). Poll get_request until decided.", + inputSchema: { type: "object", required: ["title", "changes"], properties: commonProps }, + async handler(args, { agent }) { + const input = buildInput(args); + if ("error" in input) return errResult(input.error); + return fromService(await createChangeRequest(agent, input)); + }, +}; + +export const updateRequest: McpToolDef = { + name: "update_request", + description: + "Update a PENDING or CHANGES_REQUESTED request with revised content (e.g. after the human requested changes). Resets the request to PENDING for re-review.", + inputSchema: { + type: "object", + required: ["request_id", "title", "changes"], + properties: { request_id: { type: "string" }, ...commonProps }, + }, + async handler(args, { agent }) { + const requestId = typeof args.request_id === "string" ? args.request_id : undefined; + if (!requestId) return errResult("request_id is required"); + const input = buildInput(args); + if ("error" in input) return errResult(input.error); + return fromService(await updateChangeRequest(agent, requestId, input)); + }, +}; + +export const getRequest: McpToolDef = { + name: "get_request", + description: + "Fetch a request by id: its current state, comment, and (once decided) the platform-signed receipt.", + inputSchema: { + type: "object", + required: ["request_id"], + properties: { request_id: { type: "string" } }, + }, + async handler(args, { agent }) { + const requestId = typeof args.request_id === "string" ? args.request_id : undefined; + if (!requestId) return errResult("request_id is required"); + return fromService(await getChangeRequestForAgent(agent, requestId)); + }, +}; + +export const cancelRequest: McpToolDef = { + name: "cancel_request", + description: "Cancel your own request before a decision is made (PENDING or CHANGES_REQUESTED).", + inputSchema: { + type: "object", + required: ["request_id"], + properties: { request_id: { type: "string" } }, + }, + async handler(args, { agent }) { + const requestId = typeof args.request_id === "string" ? args.request_id : undefined; + if (!requestId) return errResult("request_id is required"); + return fromService(await cancelChangeRequest(agent, requestId)); + }, +}; + +export const consumeApprovalTool: McpToolDef = { + name: "consume_approval", + description: + "Consume an APPROVED request (single-use) before applying changes. Returns the signed receipt. Fails if not APPROVED or already consumed.", + inputSchema: { + type: "object", + required: ["request_id"], + properties: { request_id: { type: "string" } }, + }, + async handler(args, { agent }) { + const requestId = typeof args.request_id === "string" ? args.request_id : undefined; + if (!requestId) return errResult("request_id is required"); + return fromService(await consumeApproval(agent, requestId)); + }, +}; + +export const listRequests: McpToolDef = { + name: "list_requests", + description: "List your recent change requests, optionally filtered by state.", + inputSchema: { + type: "object", + properties: { + state: { + type: "string", + enum: [ + "PENDING", + "CHANGES_REQUESTED", + "APPROVED", + "REJECTED", + "EXPIRED", + "CONSUMED", + "CANCELLED", + ], + }, + page: { type: "integer" }, + page_size: { type: "integer" }, + }, + }, + async handler(args, { agent }) { + const data = await listChangeRequestsForAgent(agent, { + state: typeof args.state === "string" ? args.state : undefined, + page: typeof args.page === "number" ? args.page : 1, + pageSize: typeof args.page_size === "number" ? args.page_size : 20, + }); + return json(data); + }, +}; diff --git a/Backend/src/lib/middlewares/auth.ts b/Backend/src/lib/middlewares/auth.ts new file mode 100644 index 0000000..31424f8 --- /dev/null +++ b/Backend/src/lib/middlewares/auth.ts @@ -0,0 +1,45 @@ +import { Middleware } from "rjweb-server"; +import { checkAuthentication, AuthState } from "../Authentication"; +import { SESSION_COOKIE, API_KEY_HEADER } from "../static"; +import { createLogger } from "../logger"; + +const log = createLogger("AUTH"); + +type AuthContext = { + auth?: AuthState; +}; + +/** + * Resolves auth for every request and exposes `ctr.getAuth()`. Does NOT reject — + * route handlers decide whether a session or agent key is required. This keeps + * anonymous routes (login, register, health) working while giving every handler a + * uniform way to read the caller's identity. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export const authResolutionMiddleware = new Middleware<{}, AuthContext>( + "Auth Resolution Middleware", + "1.0.0", +) + .load(() => { + log.info("Auth resolution middleware loaded"); + }) + .httpRequest(async (_config, _server, context, ctr) => { + const cookieToken = ctr.cookies.get(SESSION_COOKIE); + const apiKeyToken = ctr.headers.get(API_KEY_HEADER); + const result = await checkAuthentication(cookieToken, apiKeyToken); + const data = context.data(authResolutionMiddleware); + data.auth = result; + }) + .httpRequestContext( + (_config, Original) => + class extends Original { + getAuth(): AuthState { + const data = this.context.data(authResolutionMiddleware); + if (!data.auth) { + return { success: false, message: "Auth not resolved", method: "none" }; + } + return data.auth; + } + }, + ) + .export(); diff --git a/Backend/src/lib/middlewares/cors.ts b/Backend/src/lib/middlewares/cors.ts new file mode 100644 index 0000000..c39657f --- /dev/null +++ b/Backend/src/lib/middlewares/cors.ts @@ -0,0 +1,66 @@ +import { Middleware } from "rjweb-server"; +import { createLogger } from "../logger"; +import { env } from "../env"; + +const corsLog = createLogger("CORS"); + +const CORS_DOMAINS: string[] = []; + +export function initCorsDomains(domains: string[]) { + CORS_DOMAINS.length = 0; + CORS_DOMAINS.push(...domains.filter(Boolean)); +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export const corsMiddleware = new Middleware<{}, {}>("Custom CORS", "1.0.0") + .load(() => { + corsLog.info("Custom CORS loaded"); + }) + .httpRequest(async (_config, _server, _context, ctr, end) => { + if (env.RATELIMIT === 0) { + ctr.skipRateLimit(); + } + + if (ctr.url.path === "/api/openapi.json") { + ctr.headers.set("Content-Type", "application/json"); + ctr.headers.set("Access-Control-Allow-Origin", "*"); + return; + } + + const origin = ctr.headers.get("origin"); + + if (origin && !CORS_DOMAINS.includes(origin)) { + // Agent/API traffic (no browser origin) is unaffected; only browser + // requests from disallowed origins are blocked. + corsLog.warn({ origin }, "CORS denied"); + return end( + ctr.status(ctr.$status.FORBIDDEN).print({ + status: "FAILED", + message: "CORS policy: this origin is not allowed", + }), + ); + } + + const allowedHeaders = + ctr.headers.get("access-control-request-headers") || "content-type, x-api-key"; + const allowedMethods = "GET, POST, PUT, DELETE, OPTIONS, PATCH"; + + if (origin) { + if (ctr.url.method === "OPTIONS") { + ctr.headers.set("Access-Control-Max-Age", "86400"); + ctr.headers.set("Content-Length", "0"); + ctr.headers.set("Access-Control-Allow-Origin", origin); + ctr.headers.set("Access-Control-Allow-Methods", allowedMethods); + ctr.headers.set("Vary", "Origin"); + ctr.headers.set("Access-Control-Allow-Headers", allowedHeaders); + ctr.headers.set("Access-Control-Allow-Credentials", "true"); + return end(ctr.status(ctr.$status.NO_CONTENT).print("")); + } + ctr.headers.set("Access-Control-Allow-Origin", origin); + ctr.headers.set("Vary", "Origin"); + ctr.headers.set("Access-Control-Allow-Methods", allowedMethods); + ctr.headers.set("Access-Control-Allow-Headers", allowedHeaders); + ctr.headers.set("Access-Control-Allow-Credentials", "true"); + } + }) + .export(); diff --git a/Backend/src/lib/middlewares/main.ts b/Backend/src/lib/middlewares/main.ts new file mode 100644 index 0000000..d9a4e1c --- /dev/null +++ b/Backend/src/lib/middlewares/main.ts @@ -0,0 +1,32 @@ +import { Middleware } from "rjweb-server"; +import { createLogger } from "../logger"; +import { env } from "../env"; +import { IGNORE_PATHS, INJECT_HEADERS } from "../static"; + +const log = createLogger("HTTP"); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export const mainMiddleware = new Middleware<{}, {}>("Main Middleware", "1.0.0") + .load(() => { + log.info("Main middleware loaded"); + }) + .httpRequest(async (_config, _server, _context, ctr) => { + if (env.REQUEST_DEBUGGING && !IGNORE_PATHS.some((p) => ctr.url.href.startsWith(p))) { + log.info( + { method: ctr.url.method, url: ctr.url.href, ip: ctr.client.ip.usual() }, + "Received request", + ); + } + for (const [header, value] of Object.entries(INJECT_HEADERS)) { + ctr.headers.set(header, value); + } + }) + .httpRequestFinish(async (_config, _server, _context, ctr, ms) => { + if (env.RESPONSE_DEBUGGING && !IGNORE_PATHS.some((p) => ctr.url.href.startsWith(p))) { + log.info( + { method: ctr.url.method, url: ctr.url.href, duration: ms.toFixed(2) }, + "Sent response", + ); + } + }) + .export(); diff --git a/Backend/src/lib/response.ts b/Backend/src/lib/response.ts new file mode 100644 index 0000000..9b7adea --- /dev/null +++ b/Backend/src/lib/response.ts @@ -0,0 +1,46 @@ +import { ERROR_MESSAGES } from "./errors"; + +type ResponseContent = + | { code: number; message?: string; data?: unknown } + | { status: number; message?: string; data?: unknown }; + +function resolve(content: ResponseContent) { + const code = "code" in content ? content.code : content.status; + const message = code >= 500 ? ERROR_MESSAGES.INTERNAL_SERVER_ERROR.message : content.message; + return { code, message }; +} + +function buildBody(code: number, message: string | undefined, data: unknown) { + if (code >= 400) { + return { status: "FAILED", message }; + } + + return { + status: "OK", + ...(message !== undefined ? { message } : {}), + ...(data !== undefined ? { data } : {}), + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function makeResponse({ ctr, content }: { ctr: any; content: ResponseContent }) { + const { code, message } = resolve(content); + const data = "data" in content ? content.data : undefined; + return ctr.status(code).print(buildBody(code, message, data)); +} + +export async function endResponse({ + ctr, + end, + content, +}: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ctr: any; + end: () => void; + content: ResponseContent; +}) { + const { code, message } = resolve(content); + const data = "data" in content ? content.data : undefined; + ctr.status(code).print(buildBody(code, message, data)); + end(); +} diff --git a/Backend/src/lib/static.ts b/Backend/src/lib/static.ts new file mode 100644 index 0000000..d6556a2 --- /dev/null +++ b/Backend/src/lib/static.ts @@ -0,0 +1,17 @@ +export const IGNORE_PATHS: string[] = ["/api/openapi.json", "/api/health", "/health"]; + +export const INJECT_HEADERS: Record = { + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", +}; + +// API key prefix used for all agent API keys. +export const AGENT_KEY_PREFIX = "pp_agent_"; + +// Cookie name for human browser sessions. +export const SESSION_COOKIE = "patchpass_session"; + +// Header agents use to present their API key. +export const API_KEY_HEADER = "x-api-key"; diff --git a/Backend/src/routes/api/account.ts b/Backend/src/routes/api/account.ts new file mode 100644 index 0000000..6b32de1 --- /dev/null +++ b/Backend/src/routes/api/account.ts @@ -0,0 +1,180 @@ +import { Cookie } from "rjweb-server"; +import * as bcrypt from "bcryptjs"; +import { DOMAIN, fileRouter, prisma } from "../.."; +import { SESSION_COOKIE } from "../../lib/static"; +import { cookieDomain } from "../../lib/Authentication"; +import { requireSession } from "../../lib/RouteAuth"; +import { clampAutoDeleteDays, MIN_AUTO_DELETE_DAYS } from "../../lib/Limits"; +import { recordAudit } from "../../lib/Audit"; +import { serializeAgent } from "../../lib/AgentService"; +import { serializeRequest } from "../../lib/ChangeRequestService"; +import { createLogger } from "../../lib/logger"; + +const log = createLogger("account"); + +export = new fileRouter.Path("/") + // ── Update profile (display name) ────────────────────────────────────────── + .http("PATCH", "/api/account/profile", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => + z.object({ display_name: z.string().min(1).max(64) }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + await prisma.user.update({ + where: { id: session.user.id }, + data: { displayName: data.display_name.trim() }, + }); + return ctr.print({ status: "OK", message: "Profile updated" }); + }), + ) + // ── Change password ───────────────────────────────────────────────────────── + .http("POST", "/api/account/password", (http) => + http + .ratelimit((limit) => limit.hits(5).window(60000).penalty(3000)) + .onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => + z.object({ + current_password: z.string().min(1), + new_password: z.string().min(8).max(120), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + const match = await bcrypt.compare(data.current_password, session.user.password); + if (!match) { + return ctr + .status(ctr.$status.UNAUTHORIZED) + .print({ status: "FAILED", message: "Current password is incorrect" }); + } + const hash = await bcrypt.hash(data.new_password, 10); + await prisma.user.update({ where: { id: session.user.id }, data: { password: hash } }); + await recordAudit({ actorId: session.user.id, action: "password_changed" }); + return ctr.print({ status: "OK", message: "Password changed" }); + }), + ) + // ── Auto-delete settings ──────────────────────────────────────────────────── + .http("PATCH", "/api/account/settings", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => + z.object({ + auto_delete_enabled: z.boolean().optional(), + auto_delete_days: z.number().int().min(MIN_AUTO_DELETE_DAYS).max(3650).optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + const updated = await prisma.user.update({ + where: { id: session.user.id }, + data: { + ...(data.auto_delete_enabled !== undefined + ? { autoDeleteEnabled: data.auto_delete_enabled } + : {}), + ...(data.auto_delete_days !== undefined + ? { autoDeleteDays: clampAutoDeleteDays(data.auto_delete_days) } + : {}), + }, + }); + return ctr.print({ + status: "OK", + data: { + auto_delete_enabled: updated.autoDeleteEnabled, + auto_delete_days: updated.autoDeleteDays, + }, + }); + }), + ) + // ── GDPR: export all data ──────────────────────────────────────────────────── + .http("GET", "/api/account/export", (http) => + http + .ratelimit((limit) => limit.hits(3).window(60000).penalty(5000)) + .onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + include: { + agents: true, + changeRequests: { include: { agent: true } }, + notifications: true, + }, + }); + if (!user) return; + + const exportData = { + exported_at: new Date().toISOString(), + account: { + id: user.id, + username: user.username, + display_name: user.displayName, + role: user.role, + two_factor_enabled: user.totpEnabled, + auto_delete_enabled: user.autoDeleteEnabled, + auto_delete_days: user.autoDeleteDays, + created_at: user.createdAt.toISOString(), + }, + agents: user.agents.map((a) => serializeAgent(a)), + change_requests: user.changeRequests.map((r) => serializeRequest(r)), + notifications: user.notifications.map((n) => ({ + id: n.id, + type: n.type, + title: n.title, + message: n.message, + request_id: n.requestPublicId, + read: n.read, + created_at: n.createdAt.toISOString(), + })), + }; + + await recordAudit({ actorId: user.id, action: "data_exported" }); + ctr.headers.set("Content-Type", "application/json"); + ctr.headers.set( + "Content-Disposition", + `attachment; filename="patchpass-export-${user.username}.json"`, + ); + return ctr.print(exportData); + }), + ) + // ── GDPR: delete account (cascades to agents + requests + notifications) ────── + .http("DELETE", "/api/account", (http) => + http + .ratelimit((limit) => limit.hits(3).window(60000).penalty(5000)) + .onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => + z.object({ password: z.string().min(1), confirm: z.literal("DELETE") }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + const match = await bcrypt.compare(data.password, session.user.password); + if (!match) { + return ctr + .status(ctr.$status.UNAUTHORIZED) + .print({ status: "FAILED", message: "Password is incorrect" }); + } + + // Guard: don't let the last remaining admin delete the platform's only admin. + if (session.user.role === "ADMIN") { + const adminCount = await prisma.user.count({ where: { role: "ADMIN" } }); + if (adminCount <= 1) { + return ctr.status(ctr.$status.CONFLICT).print({ + status: "FAILED", + message: + "You are the only administrator. Promote another admin before deleting your account.", + }); + } + } + + await prisma.user.delete({ where: { id: session.user.id } }); + log.info({ userId: session.user.id }, "Account deleted (GDPR)"); + ctr.cookies.set( + SESSION_COOKIE, + new Cookie("", { domain: cookieDomain(DOMAIN), path: "/", expires: new Date(0) }), + ); + return ctr.print({ status: "OK", message: "Your account and all data have been deleted" }); + }), + ); diff --git a/Backend/src/routes/api/admin.ts b/Backend/src/routes/api/admin.ts new file mode 100644 index 0000000..0965526 --- /dev/null +++ b/Backend/src/routes/api/admin.ts @@ -0,0 +1,313 @@ +import { fileRouter, prisma } from "../.."; +import { requireAdmin } from "../../lib/RouteAuth"; +import { getGlobalSettings, setRegistrationEnabled, setRequestsEnabled } from "../../lib/DataManager"; +import { recordAudit } from "../../lib/Audit"; +import { createNotification } from "../../lib/Notifications"; +import { serializeAgent } from "../../lib/AgentService"; +import { serializeRequest } from "../../lib/ChangeRequestService"; +import { createLogger } from "../../lib/logger"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const log = createLogger("admin"); + +export = new fileRouter.Path("/") + // ── Users ──────────────────────────────────────────────────────────────── + .http("GET", "/api/admin/users", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const users = await prisma.user.findMany({ + orderBy: { createdAt: "asc" }, + select: { + id: true, + username: true, + displayName: true, + role: true, + disabled: true, + totpEnabled: true, + createdAt: true, + _count: { select: { agents: true, changeRequests: true } }, + }, + }); + return ctr.print({ + status: "OK", + data: users.map((u) => ({ + id: u.id, + username: u.username, + display_name: u.displayName, + role: u.role, + disabled: u.disabled, + totp_enabled: u.totpEnabled, + created_at: u.createdAt.toISOString(), + agent_count: u._count.agents, + request_count: u._count.changeRequests, + })), + }); + }), + ) + // Update a user: role and/or disabled. + .http("PATCH", "/api/admin/users/{id}", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid id" }); + } + const [data, error] = await ctr.bindBody((z) => + z.object({ + role: z.enum(["ADMIN", "USER"]).optional(), + disabled: z.boolean().optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + + const target = await prisma.user.findUnique({ where: { id } }); + if (!target) { + return ctr.status(ctr.$status.NOT_FOUND).print({ status: "FAILED", message: "User not found" }); + } + + // Don't allow removing the last admin. + if (data.role === "USER" && target.role === "ADMIN") { + const adminCount = await prisma.user.count({ where: { role: "ADMIN" } }); + if (adminCount <= 1) { + return ctr + .status(ctr.$status.CONFLICT) + .print({ status: "FAILED", message: "Cannot demote the last administrator" }); + } + } + + const updated = await prisma.user.update({ + where: { id }, + data: { + ...(data.role !== undefined ? { role: data.role } : {}), + ...(data.disabled !== undefined ? { disabled: data.disabled } : {}), + }, + }); + await recordAudit({ + actorId: admin.user.id, + action: "admin_user_updated", + targetType: "user", + targetId: String(id), + detail: JSON.stringify(data), + }); + if (data.disabled !== undefined || data.role !== undefined) { + await createNotification({ + userId: id, + type: "ADMIN_ACTION", + title: "Account changed by administrator", + message: + data.disabled === true + ? "An administrator disabled your account" + : data.disabled === false + ? "An administrator re-enabled your account" + : `An administrator set your role to ${data.role}`, + }); + } + return ctr.print({ + status: "OK", + data: { id: updated.id, role: updated.role, disabled: updated.disabled }, + }); + }), + ) + // Delete a user (cascades). + .http("DELETE", "/api/admin/users/{id}", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid id" }); + } + const target = await prisma.user.findUnique({ where: { id } }); + if (!target) { + return ctr.status(ctr.$status.NOT_FOUND).print({ status: "FAILED", message: "User not found" }); + } + if (target.role === "ADMIN") { + const adminCount = await prisma.user.count({ where: { role: "ADMIN" } }); + if (adminCount <= 1) { + return ctr + .status(ctr.$status.CONFLICT) + .print({ status: "FAILED", message: "Cannot delete the last administrator" }); + } + } + await prisma.user.delete({ where: { id } }); + await recordAudit({ + actorId: admin.user.id, + action: "admin_user_deleted", + targetType: "user", + targetId: String(id), + }); + log.info({ adminId: admin.user.id, deletedUserId: id }, "Admin deleted user"); + return ctr.print({ status: "OK", message: "User deleted" }); + }), + ) + // ── Global settings ────────────────────────────────────────────────────── + .http("GET", "/api/admin/settings", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + return ctr.print({ status: "OK", data: await getGlobalSettings() }); + }), + ) + .http("PATCH", "/api/admin/settings", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const [data, error] = await ctr.bindBody((z) => + z.object({ + registration_enabled: z.boolean().optional(), + requests_enabled: z.boolean().optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + if (data.registration_enabled !== undefined) await setRegistrationEnabled(data.registration_enabled); + if (data.requests_enabled !== undefined) await setRequestsEnabled(data.requests_enabled); + await recordAudit({ + actorId: admin.user.id, + action: "admin_settings_updated", + detail: JSON.stringify(data), + }); + return ctr.print({ status: "OK", data: await getGlobalSettings() }); + }), + ) + // ── Agents (platform-wide) ──────────────────────────────────────────────── + .http("GET", "/api/admin/agents", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const agents = await prisma.agent.findMany({ + orderBy: { createdAt: "asc" }, + include: { owner: { select: { username: true, id: true } } }, + }); + return ctr.print({ + status: "OK", + data: agents.map((a) => ({ + ...serializeAgent(a), + owner: { id: a.owner.id, username: a.owner.username }, + })), + }); + }), + ) + .http("POST", "/api/admin/agents/{id}/disabled", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid id" }); + } + const [data, error] = await ctr.bindBody((z) => z.object({ disabled: z.boolean() })); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + const agent = await prisma.agent.findUnique({ where: { id } }); + if (!agent) { + return ctr.status(ctr.$status.NOT_FOUND).print({ status: "FAILED", message: "Agent not found" }); + } + await prisma.agent.update({ where: { id }, data: { disabled: data.disabled } }); + await recordAudit({ + actorId: admin.user.id, + action: "admin_agent_disabled", + targetType: "agent", + targetId: String(id), + detail: JSON.stringify(data), + }); + await createNotification({ + userId: agent.ownerId, + type: "AGENT_DISABLED", + title: data.disabled ? "Agent disabled by administrator" : "Agent re-enabled by administrator", + message: `An administrator ${data.disabled ? "disabled" : "re-enabled"} your agent "${agent.name}"`, + }); + return ctr.print({ status: "OK" }); + }), + ) + .http("DELETE", "/api/admin/agents/{id}", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid id" }); + } + const agent = await prisma.agent.findUnique({ where: { id } }); + if (!agent) { + return ctr.status(ctr.$status.NOT_FOUND).print({ status: "FAILED", message: "Agent not found" }); + } + await prisma.agent.delete({ where: { id } }); + await recordAudit({ + actorId: admin.user.id, + action: "admin_agent_deleted", + targetType: "agent", + targetId: String(id), + }); + return ctr.print({ status: "OK", message: "Agent deleted" }); + }), + ) + // ── Audit logs (paginated) ──────────────────────────────────────────────── + .http("GET", "/api/admin/audit-logs", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const page = Math.max(1, Number(ctr.queries.get("page") ?? "1") || 1); + const pageSize = Math.min(100, Math.max(1, Number(ctr.queries.get("page_size") ?? "30") || 30)); + const [total, rows] = await Promise.all([ + prisma.auditLog.count(), + prisma.auditLog.findMany({ + orderBy: { createdAt: "desc" }, + skip: (page - 1) * pageSize, + take: pageSize, + include: { actor: { select: { username: true, id: true } } }, + }), + ]); + return ctr.print({ + status: "OK", + data: { + page, + page_size: pageSize, + total, + logs: rows.map((l) => ({ + id: l.id, + action: l.action, + detail: l.detail, + target_type: l.targetType, + target_id: l.targetId, + actor: l.actor ? { id: l.actor.id, username: l.actor.username } : null, + created_at: l.createdAt.toISOString(), + })), + }, + }); + }), + ) + // ── View a user's request payload (explicit + audited) ──────────────────── + .http("GET", "/api/admin/change-requests/{id}", (http) => + http.onRequest(async (ctr) => { + const admin = requireAdmin(ctr); + if (!admin) return; + const publicId = ctr.params.get("id"); + if (!publicId) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Missing id" }); + } + const request = await prisma.changeRequest.findUnique({ + where: { publicId }, + include: { agent: true, user: { select: { id: true, username: true } } }, + }); + if (!request) { + return ctr.status(ctr.$status.NOT_FOUND).print({ status: "FAILED", message: "Not found" }); + } + // Viewing another user's payload is a privileged action — always audited. + await recordAudit({ + actorId: admin.user.id, + action: "admin_viewed_request_payload", + targetType: "change_request", + targetId: publicId, + detail: `Owner user ${request.user.id} (${request.user.username})`, + }); + return ctr.print({ + status: "OK", + data: { + ...serializeRequest(request), + owner: { id: request.user.id, username: request.user.username }, + agent: request.agent ? { id: request.agent.id, name: request.agent.name } : null, + }, + }); + }), + ); diff --git a/Backend/src/routes/api/agents.ts b/Backend/src/routes/api/agents.ts new file mode 100644 index 0000000..bccd5bc --- /dev/null +++ b/Backend/src/routes/api/agents.ts @@ -0,0 +1,139 @@ +import { fileRouter } from "../.."; +import { requireSession } from "../../lib/RouteAuth"; +import { + createAgent, + deleteAgent, + listAgents, + regenerateApiKey, + setAgentDisabled, + updateAgent, +} from "../../lib/AgentService"; +import { recordAudit } from "../../lib/Audit"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function reply(ctr: any, result: { ok: boolean; status?: number; message?: string; data?: unknown }) { + if (!result.ok) { + return ctr.status(result.status ?? 400).print({ status: "FAILED", message: result.message }); + } + return ctr.print({ status: "OK", data: (result as any).data }); +} + +export = new fileRouter.Path("/") + // List agents + .http("GET", "/api/agents", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const agents = await listAgents(session.user); + return ctr.print({ status: "OK", data: agents }); + }), + ) + // Create agent + .http("POST", "/api/agents", (http) => + http + .ratelimit((limit) => limit.hits(10).window(60000).penalty(2000)) + .onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => + z.object({ + name: z.string().min(1).max(128), + description: z.string().max(500).nullable().optional(), + website: z.string().max(300).nullable().optional(), + icon_url: z.string().max(1000).nullable().optional(), + max_pending_requests: z.number().int().min(1).max(10).optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + const result = await createAgent(session.user, data); + if (result.ok) { + await recordAudit({ + actorId: session.user.id, + action: "agent_created", + targetType: "agent", + targetId: String((result.data as any).id), + }); + } + return reply(ctr, result); + }), + ) + // Update agent + .http("PATCH", "/api/agents/{id}", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid agent id" }); + } + const [data, error] = await ctr.bindBody((z) => + z.object({ + name: z.string().min(1).max(128).optional(), + description: z.string().max(500).nullable().optional(), + website: z.string().max(300).nullable().optional(), + icon_url: z.string().max(1000).nullable().optional(), + max_pending_requests: z.number().int().min(1).max(10).optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + return reply(ctr, await updateAgent(session.user, id, data)); + }), + ) + // Regenerate API key + .http("POST", "/api/agents/{id}/regenerate-key", (http) => + http + .ratelimit((limit) => limit.hits(10).window(60000).penalty(2000)) + .onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid agent id" }); + } + const result = await regenerateApiKey(session.user, id); + if (result.ok) { + await recordAudit({ + actorId: session.user.id, + action: "agent_key_regenerated", + targetType: "agent", + targetId: String(id), + }); + } + return reply(ctr, result); + }), + ) + // Enable / disable agent + .http("POST", "/api/agents/{id}/disabled", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid agent id" }); + } + const [data, error] = await ctr.bindBody((z) => z.object({ disabled: z.boolean() })); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + return reply(ctr, await setAgentDisabled(session.user, id, data.disabled)); + }), + ) + // Delete agent + .http("DELETE", "/api/agents/{id}", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid agent id" }); + } + const result = await deleteAgent(session.user, id); + if (result.ok) { + await recordAudit({ + actorId: session.user.id, + action: "agent_deleted", + targetType: "agent", + targetId: String(id), + }); + } + return reply(ctr, result); + }), + ); diff --git a/Backend/src/routes/api/auth.ts b/Backend/src/routes/api/auth.ts new file mode 100644 index 0000000..5a552b2 --- /dev/null +++ b/Backend/src/routes/api/auth.ts @@ -0,0 +1,242 @@ +import { Cookie } from "rjweb-server"; +import * as bcrypt from "bcryptjs"; +import { authenticator } from "otplib"; +import { DOMAIN, fileRouter, prisma } from "../.."; +import { SESSION_COOKIE } from "../../lib/static"; +import { + cookieDomain, + generateSessionHash, + normalizeUsername, +} from "../../lib/Authentication"; +import { getRegistrationEnabled } from "../../lib/DataManager"; +import { requireSession } from "../../lib/RouteAuth"; +import { recordAudit } from "../../lib/Audit"; +import { createLogger } from "../../lib/logger"; + +const log = createLogger("auth"); + +function setSessionCookie(ctr: any, hash: string) { + ctr.cookies.set( + SESSION_COOKIE, + new Cookie(hash, { + domain: cookieDomain(DOMAIN), + httpOnly: true, + path: "/", + sameSite: "lax", + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days + }), + ); +} + +function publicUser(user: { + id: number; + username: string; + displayName: string; + role: string; + totpEnabled: boolean; + autoDeleteEnabled: boolean; + autoDeleteDays: number; +}) { + return { + id: user.id, + username: user.username, + display_name: user.displayName, + role: user.role, + totp_enabled: user.totpEnabled, + auto_delete_enabled: user.autoDeleteEnabled, + auto_delete_days: user.autoDeleteDays, + }; +} + +export = new fileRouter.Path("/") + // ── Register ────────────────────────────────────────────────────────────── + .http("POST", "/api/auth/register", (http) => + http + .ratelimit((limit) => limit.hits(5).window(60000).penalty(2000)) + .onRequest(async (ctr) => { + const [data, error] = await ctr.bindBody((z) => + z.object({ + username: z.string().min(3).max(32), + display_name: z.string().min(1).max(64).optional(), + password: z.string().min(8).max(120), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + + if (!(await getRegistrationEnabled())) { + // First user always allowed so the platform can be bootstrapped. + const userCount = await prisma.user.count(); + if (userCount > 0) { + return ctr + .status(ctr.$status.FORBIDDEN) + .print({ status: "FAILED", message: "Registration is disabled" }); + } + } + + const username = normalizeUsername(data.username); + if (!/^[a-z0-9_.-]+$/.test(username)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ + status: "FAILED", + message: "Username may only contain letters, numbers, dots, dashes, underscores", + }); + } + + const existing = await prisma.user.findUnique({ where: { username } }); + if (existing) { + return ctr + .status(ctr.$status.CONFLICT) + .print({ status: "FAILED", message: "Username already taken" }); + } + + const isFirstUser = (await prisma.user.count()) === 0; + const passwordHash = await bcrypt.hash(data.password, 10); + const hash = generateSessionHash(username); + + const user = await prisma.user.create({ + data: { + username, + displayName: (data.display_name || data.username).trim(), + password: passwordHash, + role: isFirstUser ? "ADMIN" : "USER", + sessions: { create: { hash } }, + }, + }); + + setSessionCookie(ctr, hash); + log.info({ userId: user.id, admin: isFirstUser }, "User registered"); + return ctr.print({ status: "OK", message: "Welcome to PatchPass!", data: publicUser(user) }); + }), + ) + // ── Login ───────────────────────────────────────────────────────────────── + .http("POST", "/api/auth/login", (http) => + http + .ratelimit((limit) => limit.hits(6).window(10000).penalty(0)) + .onRequest(async (ctr) => { + const [data, error] = await ctr.bindBody((z) => + z.object({ + username: z.string().min(1).max(64), + password: z.string().min(1).max(120), + totp: z.string().min(6).max(6).optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + + const username = normalizeUsername(data.username); + const user = await prisma.user.findUnique({ where: { username } }); + if (!user) { + return ctr + .status(ctr.$status.UNAUTHORIZED) + .print({ status: "FAILED", message: "Invalid credentials" }); + } + if (user.disabled) { + return ctr + .status(ctr.$status.FORBIDDEN) + .print({ status: "FAILED", message: "This account has been disabled" }); + } + + const match = await bcrypt.compare(data.password, user.password); + if (!match) { + return ctr + .status(ctr.$status.UNAUTHORIZED) + .print({ status: "FAILED", message: "Invalid credentials" }); + } + + if (user.totpEnabled && user.totpSecret) { + if (!data.totp) { + return ctr + .status(ctr.$status.UNAUTHORIZED) + .print({ status: "FAILED", message: "2FA code required", data: { totp_required: true } }); + } + const valid = authenticator.verify({ token: data.totp, secret: user.totpSecret }); + if (!valid) { + return ctr + .status(ctr.$status.UNAUTHORIZED) + .print({ status: "FAILED", message: "Invalid 2FA code" }); + } + } + + const hash = generateSessionHash(username); + await prisma.session.create({ data: { userId: user.id, hash } }); + setSessionCookie(ctr, hash); + return ctr.print({ status: "OK", message: "Welcome back!", data: publicUser(user) }); + }), + ) + // ── Logout ──────────────────────────────────────────────────────────────── + .http("POST", "/api/auth/logout", (http) => + http.onRequest(async (ctr) => { + const hash = ctr.cookies.get(SESSION_COOKIE); + if (hash) { + await prisma.session.deleteMany({ where: { hash } }); + } + ctr.cookies.set( + SESSION_COOKIE, + new Cookie("", { domain: cookieDomain(DOMAIN), path: "/", expires: new Date(0) }), + ); + return ctr.print({ status: "OK", message: "Logged out" }); + }), + ) + // ── Current user ────────────────────────────────────────────────────────── + .http("GET", "/api/auth/me", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + return ctr.print({ status: "OK", data: publicUser(session.user) }); + }), + ) + // ── 2FA: begin setup (returns secret + otpauth URL) ───────────────────────── + .http("POST", "/api/auth/2fa/setup", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + if (session.user.totpEnabled) { + return ctr + .status(ctr.$status.BAD_REQUEST) + .print({ status: "FAILED", message: "2FA is already enabled" }); + } + const secret = authenticator.generateSecret(); + await prisma.user.update({ where: { id: session.user.id }, data: { totpSecret: secret } }); + const otpauth = authenticator.keyuri(session.user.username, "PatchPass", secret); + return ctr.print({ status: "OK", data: { secret, otpauth_url: otpauth } }); + }), + ) + // ── 2FA: confirm & enable ─────────────────────────────────────────────────── + .http("POST", "/api/auth/2fa/enable", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => z.object({ totp: z.string().min(6).max(6) })); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + + const fresh = await prisma.user.findUnique({ where: { id: session.user.id } }); + if (!fresh?.totpSecret) { + return ctr + .status(ctr.$status.BAD_REQUEST) + .print({ status: "FAILED", message: "Start 2FA setup first" }); + } + if (!authenticator.verify({ token: data.totp, secret: fresh.totpSecret })) { + return ctr.status(ctr.$status.UNAUTHORIZED).print({ status: "FAILED", message: "Invalid code" }); + } + await prisma.user.update({ where: { id: fresh.id }, data: { totpEnabled: true } }); + await recordAudit({ actorId: fresh.id, action: "2fa_enabled" }); + return ctr.print({ status: "OK", message: "Two-factor authentication enabled" }); + }), + ) + // ── 2FA: disable ──────────────────────────────────────────────────────────── + .http("POST", "/api/auth/2fa/disable", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const [data, error] = await ctr.bindBody((z) => z.object({ password: z.string().min(1) })); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + const match = await bcrypt.compare(data.password, session.user.password); + if (!match) { + return ctr.status(ctr.$status.UNAUTHORIZED).print({ status: "FAILED", message: "Invalid password" }); + } + await prisma.user.update({ + where: { id: session.user.id }, + data: { totpEnabled: false, totpSecret: null }, + }); + await recordAudit({ actorId: session.user.id, action: "2fa_disabled" }); + return ctr.print({ status: "OK", message: "Two-factor authentication disabled" }); + }), + ); diff --git a/Backend/src/routes/api/changeRequests.ts b/Backend/src/routes/api/changeRequests.ts new file mode 100644 index 0000000..b62defd --- /dev/null +++ b/Backend/src/routes/api/changeRequests.ts @@ -0,0 +1,143 @@ +import { fileRouter, prisma } from "../.."; +import { requireSession } from "../../lib/RouteAuth"; +import { + DecisionKind, + decideChangeRequest, + materializeExpiry, + serializeRequest, +} from "../../lib/ChangeRequestService"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function withAgent(request: any, agent: any) { + return { + ...serializeRequest(request), + agent: agent + ? { + id: agent.id, + name: agent.name, + description: agent.description, + website: agent.website, + icon_url: agent.iconUrl, + disabled: agent.disabled, + } + : null, + }; +} + +export = new fileRouter.Path("/") + // List / history of requests owned by the human (paginated, filterable). + .http("GET", "/api/change-requests", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + + const page = Math.max(1, Number(ctr.queries.get("page") ?? "1") || 1); + const pageSize = Math.min(100, Math.max(1, Number(ctr.queries.get("page_size") ?? "20") || 20)); + const stateFilter = ctr.queries.get("state"); + const agentFilter = ctr.queries.get("agent_id"); + + const validStates = [ + "PENDING", + "CHANGES_REQUESTED", + "APPROVED", + "REJECTED", + "EXPIRED", + "CONSUMED", + "CANCELLED", + ]; + + const where: any = { userId: session.user.id }; + if (stateFilter && validStates.includes(stateFilter)) where.state = stateFilter; + if (agentFilter && !Number.isNaN(Number(agentFilter))) where.agentId = Number(agentFilter); + + const [total, rows] = await Promise.all([ + prisma.changeRequest.count({ where }), + prisma.changeRequest.findMany({ + where, + include: { agent: true }, + orderBy: { createdAt: "desc" }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + ]); + + // Materialize expiry for any rows past their deadline so the list is accurate. + const materialized = await Promise.all(rows.map((r) => materializeExpiry(r))); + return ctr.print({ + status: "OK", + data: { + page, + page_size: pageSize, + total, + requests: materialized.map((r, i) => withAgent(r, rows[i].agent)), + }, + }); + }), + ) + // Pending count summary (for dashboard badges). + .http("GET", "/api/change-requests/summary", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const grouped = await prisma.changeRequest.groupBy({ + by: ["state"], + where: { userId: session.user.id }, + _count: { _all: true }, + }); + const counts: Record = {}; + for (const g of grouped) counts[g.state] = g._count._all; + return ctr.print({ status: "OK", data: { counts } }); + }), + ) + // Get a single request (full detail). + .http("GET", "/api/change-requests/{id}", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const publicId = ctr.params.get("id"); + if (!publicId) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Missing id" }); + } + const existing = await prisma.changeRequest.findUnique({ + where: { publicId }, + include: { agent: true }, + }); + if (!existing || existing.userId !== session.user.id) { + return ctr.status(ctr.$status.NOT_FOUND).print({ status: "FAILED", message: "Not found" }); + } + const request = await materializeExpiry(existing); + return ctr.print({ status: "OK", data: withAgent(request, existing.agent) }); + }), + ) + // Decide a request: approve / reject / request_changes. + .http("POST", "/api/change-requests/{id}/decision", (http) => + http + .ratelimit((limit) => limit.hits(30).window(60000).penalty(1000)) + .onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const publicId = ctr.params.get("id"); + if (!publicId) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Missing id" }); + } + const [data, error] = await ctr.bindBody((z) => + z.object({ + decision: z.enum(["APPROVE", "REJECT", "REQUEST_CHANGES"]), + comment: z.string().max(500).nullable().optional(), + }), + ); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(error.toString()); + + const result = await decideChangeRequest( + session.user, + publicId, + data.decision as DecisionKind, + data.comment, + ); + if (!result.ok) { + return ctr.status(result.status).print({ status: "FAILED", message: result.message }); + } + return ctr.print({ status: "OK", data: result.data }); + }), + ); diff --git a/Backend/src/routes/api/global.ts b/Backend/src/routes/api/global.ts new file mode 100644 index 0000000..606f88a --- /dev/null +++ b/Backend/src/routes/api/global.ts @@ -0,0 +1,13 @@ +import { fileRouter, VERSION } from "../.."; +import { getGlobalSettings } from "../../lib/DataManager"; + +// Public, unauthenticated: lets the UI know whether registration is open etc. +export = new fileRouter.Path("/").http("GET", "/api/global", (http) => + http.onRequest(async (ctr) => { + const settings = await getGlobalSettings(); + return ctr.print({ + status: "OK", + data: { version: VERSION.toString(), ...settings }, + }); + }), +); diff --git a/Backend/src/routes/api/notifications.ts b/Backend/src/routes/api/notifications.ts new file mode 100644 index 0000000..44814c7 --- /dev/null +++ b/Backend/src/routes/api/notifications.ts @@ -0,0 +1,80 @@ +import { fileRouter, prisma } from "../.."; +import { requireSession } from "../../lib/RouteAuth"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function serialize(n: any) { + return { + id: n.id, + type: n.type, + title: n.title, + message: n.message, + request_id: n.requestPublicId, + read: n.read, + created_at: n.createdAt.toISOString(), + }; +} + +export = new fileRouter.Path("/") + // List notifications (paginated) + unread count. + .http("GET", "/api/notifications", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const page = Math.max(1, Number(ctr.queries.get("page") ?? "1") || 1); + const pageSize = Math.min(100, Math.max(1, Number(ctr.queries.get("page_size") ?? "30") || 30)); + const unreadOnly = ctr.queries.get("unread") === "true"; + const where: any = { userId: session.user.id, ...(unreadOnly ? { read: false } : {}) }; + const [total, rows, unreadCount] = await Promise.all([ + prisma.notification.count({ where }), + prisma.notification.findMany({ + where, + orderBy: { createdAt: "desc" }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + prisma.notification.count({ where: { userId: session.user.id, read: false } }), + ]); + return ctr.print({ + status: "OK", + data: { page, page_size: pageSize, total, unread_count: unreadCount, notifications: rows.map(serialize) }, + }); + }), + ) + // Mark one notification read. + .http("POST", "/api/notifications/{id}/read", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + const id = Number(ctr.params.get("id")); + if (Number.isNaN(id)) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Invalid id" }); + } + await prisma.notification.updateMany({ + where: { id, userId: session.user.id }, + data: { read: true }, + }); + return ctr.print({ status: "OK" }); + }), + ) + // Mark all read. + .http("POST", "/api/notifications/read-all", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + await prisma.notification.updateMany({ + where: { userId: session.user.id, read: false }, + data: { read: true }, + }); + return ctr.print({ status: "OK" }); + }), + ) + // Clear (delete) all notifications. + .http("DELETE", "/api/notifications", (http) => + http.onRequest(async (ctr) => { + const session = requireSession(ctr); + if (!session) return; + await prisma.notification.deleteMany({ where: { userId: session.user.id } }); + return ctr.print({ status: "OK", message: "Notifications cleared" }); + }), + ); diff --git a/Backend/src/routes/health.ts b/Backend/src/routes/health.ts new file mode 100644 index 0000000..153b810 --- /dev/null +++ b/Backend/src/routes/health.ts @@ -0,0 +1,13 @@ +import { fileRouter, VERSION } from ".."; + +export = new fileRouter.Path("/") + .http("GET", "/api/health", (http) => + http.onRequest(async (ctr) => { + return ctr.print({ status: "OK", service: "PatchPass", version: VERSION.toString() }); + }), + ) + .http("GET", "/health", (http) => + http.onRequest(async (ctr) => { + return ctr.print({ status: "OK" }); + }), + ); diff --git a/Backend/src/routes/mcp.ts b/Backend/src/routes/mcp.ts new file mode 100644 index 0000000..43fe8e4 --- /dev/null +++ b/Backend/src/routes/mcp.ts @@ -0,0 +1,69 @@ +import { fileRouter } from ".."; +import { API_KEY_HEADER } from "../lib/static"; +import { checkAuthentication } from "../lib/Authentication"; +import { handleMcpRequest, McpRequest } from "../lib/mcp/mcp"; +import { tools } from "../lib/mcp/tools"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export = new fileRouter.Path("/") + // Discovery — unauthenticated metadata about the MCP server. + .http("GET", "/mcp", (http) => + http + .ratelimit((limit) => limit.hits(20).window(60000).penalty(2000)) + .onRequest(async (ctr) => { + return ctr.print({ + name: "patchpass", + version: "1.0.0", + description: "PatchPass MCP Server — request human approval for agent actions", + transport: "streamable-http", + authentication: { header: API_KEY_HEADER, type: "agent-api-key" }, + tools: tools.map(({ name, description }) => ({ name, description })), + }); + }), + ) + // JSON-RPC endpoint — authenticated with the agent API key. + .http("POST", "/mcp", (http) => + http + .ratelimit((limit) => limit.hits(120).window(60000).penalty(5000)) + .onRequest(async (ctr) => { + const apiKey = ctr.headers.get(API_KEY_HEADER); + if (!apiKey) { + return ctr.status(ctr.$status.UNAUTHORIZED).print({ + jsonrpc: "2.0", + id: null, + error: { code: -32001, message: `Authentication required: provide ${API_KEY_HEADER} header` }, + }); + } + + const auth = await checkAuthentication(null, apiKey); + if (!auth.success || auth.method !== "agent") { + return ctr.status(ctr.$status.UNAUTHORIZED).print({ + jsonrpc: "2.0", + id: null, + error: { code: -32001, message: auth.success ? "Agent key required" : auth.message }, + }); + } + + const [body, bindErr] = await ctr.bindBody((z: any) => + z.object({ + jsonrpc: z.literal("2.0"), + id: z.union([z.string(), z.number(), z.null()]).optional(), + method: z.string(), + params: z.any().optional(), + }), + ); + if (!body) { + return ctr.status(ctr.$status.BAD_REQUEST).print({ + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: `Parse error: ${bindErr}` }, + }); + } + + const { status, body: responseBody } = await handleMcpRequest(body as McpRequest, { + agent: auth.agent, + }); + return ctr.status(status).print(responseBody as any); + }), + ); diff --git a/Backend/src/routes/v1/changeRequests.ts b/Backend/src/routes/v1/changeRequests.ts new file mode 100644 index 0000000..cca70d9 --- /dev/null +++ b/Backend/src/routes/v1/changeRequests.ts @@ -0,0 +1,130 @@ +import { fileRouter } from "../.."; +import { requireAgent } from "../../lib/RouteAuth"; +import { + cancelChangeRequest, + consumeApproval, + createChangeRequest, + getChangeRequestForAgent, + listChangeRequestsForAgent, + updateChangeRequest, +} from "../../lib/ChangeRequestService"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const changeSchema = (z: any) => + z.discriminatedUnion("type", [ + z.object({ + type: z.literal("unified_diff"), + path: z.string().min(1).max(1024), + content: z.string().min(1).max(500_000), + }), + z.object({ + type: z.literal("config"), + path: z.string().min(1).max(1024), + before: z.any().optional(), + after: z.any().optional(), + content_type: z.string().max(64).optional(), + }), + z.object({ + type: z.literal("custom"), + label: z.string().min(1).max(256), + before: z.any().optional(), + after: z.any().optional(), + }), + ]); + +const createSchema = (z: any) => + z.object({ + title: z.string().min(1).max(200), + description: z.string().max(5000).nullable().optional(), + changes: z.array(changeSchema(z)).min(1).max(100), + expires_in: z.number().int().min(60).max(43200).nullable().optional(), + metadata: z.record(z.string(), z.any()).nullable().optional(), + }); + +function reply(ctr: any, result: { ok: boolean; status?: number; message?: string; data?: unknown }) { + if (!result.ok) { + return ctr.status(result.status ?? 400).print({ status: "FAILED", message: result.message }); + } + return ctr.print({ status: "OK", data: (result as any).data }); +} + +function requestId(ctr: any): string | null { + const id = ctr.params.get("id"); + if (!id) { + ctr.status(ctr.$status.BAD_REQUEST).print({ status: "FAILED", message: "Missing request id" }); + return null; + } + return id; +} + +export = new fileRouter.Path("/") + // Submit a change request. + .http("POST", "/v1/change-requests", (http) => + http + .ratelimit((limit) => limit.hits(30).window(60000).penalty(2000)) + .onRequest(async (ctr) => { + const auth = requireAgent(ctr); + if (!auth) return; + const [data, error] = await ctr.bindBody(createSchema); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(String(error)); + return reply(ctr, await createChangeRequest(auth.agent, data as any)); + }), + ) + // List this agent's requests. + .http("GET", "/v1/change-requests", (http) => + http.onRequest(async (ctr) => { + const auth = requireAgent(ctr); + if (!auth) return; + const data = await listChangeRequestsForAgent(auth.agent, { + state: ctr.queries.get("state") ?? undefined, + page: Number(ctr.queries.get("page") ?? "1") || 1, + pageSize: Number(ctr.queries.get("page_size") ?? "20") || 20, + }); + return ctr.print({ status: "OK", data }); + }), + ) + // Get a single request + receipt. + .http("GET", "/v1/change-requests/{id}", (http) => + http.onRequest(async (ctr) => { + const auth = requireAgent(ctr); + if (!auth) return; + const id = requestId(ctr); + if (!id) return; + return reply(ctr, await getChangeRequestForAgent(auth.agent, id)); + }), + ) + // Update a request (only while PENDING or CHANGES_REQUESTED). + .http("PATCH", "/v1/change-requests/{id}", (http) => + http + .ratelimit((limit) => limit.hits(30).window(60000).penalty(2000)) + .onRequest(async (ctr) => { + const auth = requireAgent(ctr); + if (!auth) return; + const id = requestId(ctr); + if (!id) return; + const [data, error] = await ctr.bindBody(createSchema); + if (!data) return ctr.status(ctr.$status.BAD_REQUEST).print(String(error)); + return reply(ctr, await updateChangeRequest(auth.agent, id, data as any)); + }), + ) + // Cancel a request before a decision. + .http("POST", "/v1/change-requests/{id}/cancel", (http) => + http.onRequest(async (ctr) => { + const auth = requireAgent(ctr); + if (!auth) return; + const id = requestId(ctr); + if (!id) return; + return reply(ctr, await cancelChangeRequest(auth.agent, id)); + }), + ) + // Consume an approval (single-use). + .http("POST", "/v1/change-requests/{id}/consume", (http) => + http.onRequest(async (ctr) => { + const auth = requireAgent(ctr); + if (!auth) return; + const id = requestId(ctr); + if (!id) return; + return reply(ctr, await consumeApproval(auth.agent, id)); + }), + ); diff --git a/Backend/src/routes/ws.ts b/Backend/src/routes/ws.ts new file mode 100644 index 0000000..27953d7 --- /dev/null +++ b/Backend/src/routes/ws.ts @@ -0,0 +1,32 @@ +import { fileRouter } from ".."; +import { checkAuthentication } from "../lib/Authentication"; +import { SESSION_COOKIE } from "../lib/static"; +import { getUserChannel } from "../lib/WsHub"; +import { createLogger } from "../lib/logger"; + +const log = createLogger("WS"); + +/** + * Realtime notifications socket. The browser connects with its session cookie; + * on open we resolve the session and subscribe the socket to that user's channel. + * Unauthenticated sockets are closed immediately. + */ +export = new fileRouter.Path("/").ws("/api/ws/notifications", (ws) => + ws + .onOpen(async (ctr) => { + const hash = ctr.cookies.get(SESSION_COOKIE); + const auth = await checkAuthentication(hash, null); + if (!auth.success || auth.method !== "session") { + ctr.close(1008, "Unauthorized"); + return; + } + ctr.printChannel(getUserChannel(auth.user.id)); + log.debug({ userId: auth.user.id }, "WS notification socket opened"); + }) + .onMessage(async (ctr) => { + // Client heartbeats — echo a pong so idle proxies keep the socket alive. + if (ctr.rawMessage("utf8") === "ping") { + ctr.print("text", JSON.stringify({ kind: "pong", at: new Date().toISOString() })); + } + }), +); diff --git a/Backend/src/tests/ChangeNormalization.test.ts b/Backend/src/tests/ChangeNormalization.test.ts new file mode 100644 index 0000000..db81a09 --- /dev/null +++ b/Backend/src/tests/ChangeNormalization.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { + canonicalize, + computeContentHash, + normalizeChanges, + normalizeDiff, +} from "../lib/ChangeNormalization"; + +describe("normalizeDiff", () => { + it("converts CRLF and lone CR to LF", () => { + expect(normalizeDiff("a\r\nb\rc")).toBe("a\nb\nc\n"); + }); + + it("collapses trailing newlines to exactly one", () => { + expect(normalizeDiff("line\n\n\n")).toBe("line\n"); + }); + + it("returns empty string for whitespace-only content", () => { + expect(normalizeDiff("\n\n")).toBe(""); + }); +}); + +describe("normalizeChanges", () => { + it("normalizes diff content and fills config/custom defaults", () => { + const out = normalizeChanges([ + { type: "unified_diff", path: "a.txt", content: "x\r\n" }, + { type: "config", path: "k", after: 5 }, + { type: "custom", label: "L" }, + ]); + expect(out[0]).toEqual({ type: "unified_diff", path: "a.txt", content: "x\n" }); + expect(out[1]).toMatchObject({ type: "config", path: "k", before: null, after: 5, content_type: null }); + expect(out[2]).toMatchObject({ type: "custom", label: "L", before: null, after: null }); + }); + + it("preserves the submitted order of changes", () => { + const out = normalizeChanges([ + { type: "custom", label: "first" }, + { type: "custom", label: "second" }, + ]); + expect(out.map((c) => (c as { label: string }).label)).toEqual(["first", "second"]); + }); +}); + +describe("canonicalize", () => { + it("is independent of key insertion order", () => { + expect(canonicalize({ a: 1, b: 2 })).toBe(canonicalize({ b: 2, a: 1 })); + }); +}); + +describe("computeContentHash", () => { + const base = { + title: "T", + description: "D", + changes: [{ type: "config" as const, path: "k", before: 1, after: 2, content_type: "integer" }], + }; + + it("is stable across equivalent inputs (CRLF vs LF, key order)", () => { + const a = computeContentHash({ + title: "T", + description: "D", + changes: [{ type: "unified_diff", path: "f", content: "a\r\nb\r\n" }], + }); + const b = computeContentHash({ + title: "T", + description: "D", + changes: [{ type: "unified_diff", path: "f", content: "a\nb" }], + }); + expect(a).toBe(b); + }); + + it("changes when the title changes", () => { + expect(computeContentHash(base)).not.toBe(computeContentHash({ ...base, title: "T2" })); + }); + + it("changes when a change value changes", () => { + const mutated = { + ...base, + changes: [{ type: "config" as const, path: "k", before: 1, after: 3, content_type: "integer" }], + }; + expect(computeContentHash(base)).not.toBe(computeContentHash(mutated)); + }); + + it("is order-sensitive across changes", () => { + const reordered = { + ...base, + changes: [ + { type: "custom" as const, label: "x", before: 1, after: 2 }, + { type: "custom" as const, label: "y", before: 3, after: 4 }, + ], + }; + const swapped = { + ...base, + changes: [ + { type: "custom" as const, label: "y", before: 3, after: 4 }, + { type: "custom" as const, label: "x", before: 1, after: 2 }, + ], + }; + expect(computeContentHash(reordered)).not.toBe(computeContentHash(swapped)); + }); +}); diff --git a/Backend/src/tests/ChangeRequestLifecycle.test.ts b/Backend/src/tests/ChangeRequestLifecycle.test.ts new file mode 100644 index 0000000..82d8369 --- /dev/null +++ b/Backend/src/tests/ChangeRequestLifecycle.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { + cancelChangeRequest, + consumeApproval, + createChangeRequest, + decideChangeRequest, + getChangeRequestForAgent, + updateChangeRequest, +} from "../lib/ChangeRequestService"; +import { verifyReceipt } from "../lib/Signing"; +import { prisma } from "../lib/db"; +import { createAgent, createUser, resetDb, sampleChanges } from "./helpers"; + +async function freshAgentAndOwner() { + const owner = await createUser(); + const agent = await createAgent(owner.id); + return { owner, agent }; +} + +const input = { title: "Deploy", description: "d", changes: sampleChanges }; + +describe("Change request lifecycle", () => { + beforeEach(async () => { + await resetDb(); + }); + + it("creates a PENDING request with a content hash and approval url", async () => { + const { agent } = await freshAgentAndOwner(); + const res = await createChangeRequest(agent, input); + expect(res.ok).toBe(true); + if (!res.ok) return; + expect(res.data.state).toBe("PENDING"); + expect(res.data.content_hash).toHaveLength(64); + expect(res.data.approval_url).toContain(res.data.request_id); + expect(res.data.receipt).toBeNull(); + }); + + it("approve → signed receipt that verifies and binds to the content hash", async () => { + const { owner, agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error("create failed"); + const decided = await decideChangeRequest(owner, created.data.request_id, "APPROVE", "ok"); + expect(decided.ok).toBe(true); + if (!decided.ok) return; + expect(decided.data.state).toBe("APPROVED"); + const receipt = decided.data.receipt!; + expect(receipt).not.toBeNull(); + expect(receipt.payload.content_hash).toBe(created.data.content_hash); + expect(verifyReceipt(receipt.payload, receipt.signature)).toBe(true); + }); + + it("consume marks CONSUMED once and refuses a second time", async () => { + const { owner, agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + await decideChangeRequest(owner, created.data.request_id, "APPROVE"); + const first = await consumeApproval(agent, created.data.request_id); + expect(first.ok).toBe(true); + if (first.ok) expect(first.data.state).toBe("CONSUMED"); + const second = await consumeApproval(agent, created.data.request_id); + expect(second.ok).toBe(false); + if (!second.ok) expect(second.status).toBe(409); + }); + + it("cannot consume a request that was not approved", async () => { + const { agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + const res = await consumeApproval(agent, created.data.request_id); + expect(res.ok).toBe(false); + }); + + it("reject is a terminal blocker; no receipt consumption possible", async () => { + const { owner, agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + const rejected = await decideChangeRequest(owner, created.data.request_id, "REJECT", "no"); + expect(rejected.ok).toBe(true); + if (rejected.ok) expect(rejected.data.state).toBe("REJECTED"); + const consumed = await consumeApproval(agent, created.data.request_id); + expect(consumed.ok).toBe(false); + }); + + it("request_changes requires a comment and enables agent update loop", async () => { + const { owner, agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + + const noComment = await decideChangeRequest(owner, created.data.request_id, "REQUEST_CHANGES"); + expect(noComment.ok).toBe(false); + + const withComment = await decideChangeRequest( + owner, + created.data.request_id, + "REQUEST_CHANGES", + "please revise", + ); + expect(withComment.ok).toBe(true); + if (withComment.ok) expect(withComment.data.state).toBe("CHANGES_REQUESTED"); + + const updated = await updateChangeRequest(agent, created.data.request_id, { + title: "Deploy v2", + changes: [{ type: "custom", label: "Command", before: "a", after: "c" }], + }); + expect(updated.ok).toBe(true); + if (!updated.ok) return; + expect(updated.data.state).toBe("PENDING"); + expect(updated.data.resubmitted).toBe(true); + expect(updated.data.update_count).toBe(1); + // content hash must change with new content + expect(updated.data.content_hash).not.toBe(created.data.content_hash); + }); + + it("no update is possible after approval", async () => { + const { owner, agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + await decideChangeRequest(owner, created.data.request_id, "APPROVE"); + const res = await updateChangeRequest(agent, created.data.request_id, input); + expect(res.ok).toBe(false); + if (!res.ok) expect(res.status).toBe(409); + }); + + it("no decision is possible after a terminal state", async () => { + const { owner, agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + await decideChangeRequest(owner, created.data.request_id, "APPROVE"); + const again = await decideChangeRequest(owner, created.data.request_id, "REJECT"); + expect(again.ok).toBe(false); + }); + + it("agent can cancel before a decision", async () => { + const { agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + const cancelled = await cancelChangeRequest(agent, created.data.request_id); + expect(cancelled.ok).toBe(true); + if (cancelled.ok) expect(cancelled.data.state).toBe("CANCELLED"); + }); + + it("expired-on-read: a past-expiry PENDING request becomes EXPIRED when fetched", async () => { + const { agent } = await freshAgentAndOwner(); + const created = await createChangeRequest(agent, input); + if (!created.ok) throw new Error(); + await prisma.changeRequest.update({ + where: { publicId: created.data.request_id }, + data: { expiresAt: new Date(Date.now() - 1000) }, + }); + const fetched = await getChangeRequestForAgent(agent, created.data.request_id); + expect(fetched.ok).toBe(true); + if (fetched.ok) expect(fetched.data.state).toBe("EXPIRED"); + }); +}); + +describe("Authorization", () => { + beforeEach(async () => { + await resetDb(); + }); + + it("an agent cannot read another agent's request", async () => { + const a = await freshAgentAndOwner(); + const b = await freshAgentAndOwner(); + const created = await createChangeRequest(a.agent, input); + if (!created.ok) throw new Error(); + const res = await getChangeRequestForAgent(b.agent, created.data.request_id); + expect(res.ok).toBe(false); + if (!res.ok) expect(res.status).toBe(404); + }); + + it("a human cannot decide a request they do not own", async () => { + const a = await freshAgentAndOwner(); + const otherHuman = await createUser(); + const created = await createChangeRequest(a.agent, input); + if (!created.ok) throw new Error(); + const res = await decideChangeRequest(otherHuman, created.data.request_id, "APPROVE"); + expect(res.ok).toBe(false); + if (!res.ok) expect(res.status).toBe(404); + }); + + it("an agent cannot cancel another agent's request", async () => { + const a = await freshAgentAndOwner(); + const b = await freshAgentAndOwner(); + const created = await createChangeRequest(a.agent, input); + if (!created.ok) throw new Error(); + const res = await cancelChangeRequest(b.agent, created.data.request_id); + expect(res.ok).toBe(false); + }); +}); diff --git a/Backend/src/tests/Limits.test.ts b/Backend/src/tests/Limits.test.ts new file mode 100644 index 0000000..93b68a4 --- /dev/null +++ b/Backend/src/tests/Limits.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { + clampAutoDeleteDays, + clampExpirySeconds, + clampPendingLimit, + checkAgentCountLimit, + checkAgentHourlyLimit, + checkAgentPendingLimit, + DEFAULT_EXPIRY_SECONDS, + MAX_AGENTS_PER_USER, + MAX_EXPIRY_SECONDS, + MAX_REQUESTS_PER_HOUR, +} from "../lib/Limits"; +import { prisma } from "../lib/db"; +import { createAgent, createUser, resetDb } from "./helpers"; + +describe("Limits — pure clamps", () => { + it("clamps pending limit to 1..10", () => { + expect(clampPendingLimit(0)).toBe(1); + expect(clampPendingLimit(5)).toBe(5); + expect(clampPendingLimit(99)).toBe(10); + expect(clampPendingLimit(NaN)).toBe(5); + }); + + it("clamps expiry to bounds and defaults sensibly", () => { + expect(clampExpirySeconds(undefined)).toBe(DEFAULT_EXPIRY_SECONDS); + expect(clampExpirySeconds(10)).toBe(60); + expect(clampExpirySeconds(99999999)).toBe(MAX_EXPIRY_SECONDS); + expect(clampExpirySeconds(3600)).toBe(3600); + }); + + it("clamps auto-delete days to a 7-day minimum", () => { + expect(clampAutoDeleteDays(1)).toBe(7); + expect(clampAutoDeleteDays(30)).toBe(30); + }); +}); + +describe("Limits — DB-backed", () => { + beforeEach(async () => { + await resetDb(); + }); + + it("blocks agent creation past the per-user cap", async () => { + const user = await createUser(); + for (let i = 0; i < MAX_AGENTS_PER_USER; i++) await createAgent(user.id); + const result = await checkAgentCountLimit(user.id); + expect(result.allowed).toBe(false); + }); + + it("allows agent creation below the cap", async () => { + const user = await createUser(); + await createAgent(user.id); + expect((await checkAgentCountLimit(user.id)).allowed).toBe(true); + }); + + it("enforces the hourly request limit", async () => { + const user = await createUser(); + const agent = await createAgent(user.id); + for (let i = 0; i < MAX_REQUESTS_PER_HOUR; i++) { + await prisma.changeRequest.create({ + data: { + title: `r${i}`, + changes: [], + rawChanges: [], + contentHash: "h", + expiresAt: new Date(Date.now() + 60000), + agentId: agent.id, + userId: user.id, + }, + }); + } + expect((await checkAgentHourlyLimit(agent.id)).allowed).toBe(false); + }); + + it("does not count requests older than an hour toward the hourly limit", async () => { + const user = await createUser(); + const agent = await createAgent(user.id); + const old = new Date(Date.now() - 2 * 60 * 60 * 1000); + for (let i = 0; i < MAX_REQUESTS_PER_HOUR; i++) { + await prisma.changeRequest.create({ + data: { + title: `r${i}`, + changes: [], + rawChanges: [], + contentHash: "h", + expiresAt: new Date(Date.now() + 60000), + agentId: agent.id, + userId: user.id, + createdAt: old, + }, + }); + } + expect((await checkAgentHourlyLimit(agent.id)).allowed).toBe(true); + }); + + it("enforces the pending limit only against PENDING requests", async () => { + const user = await createUser(); + const agent = await createAgent(user.id, { maxPendingRequests: 2 }); + const mk = (state: "PENDING" | "APPROVED") => + prisma.changeRequest.create({ + data: { + title: "r", + changes: [], + rawChanges: [], + contentHash: "h", + state, + expiresAt: new Date(Date.now() + 60000), + agentId: agent.id, + userId: user.id, + }, + }); + await mk("PENDING"); + await mk("APPROVED"); // should not count + expect((await checkAgentPendingLimit(agent.id, 2)).allowed).toBe(true); + await mk("PENDING"); + expect((await checkAgentPendingLimit(agent.id, 2)).allowed).toBe(false); + }); +}); diff --git a/Backend/src/tests/Misc.test.ts b/Backend/src/tests/Misc.test.ts new file mode 100644 index 0000000..23a5735 --- /dev/null +++ b/Backend/src/tests/Misc.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { validateIconUrl } from "../lib/IconValidation"; +import { createChangeRequest } from "../lib/ChangeRequestService"; +import { setRequestsEnabled } from "../lib/DataManager"; +import { normalizeUsername } from "../lib/Authentication"; +import { createAgent, createUser, resetDb, sampleChanges } from "./helpers"; + +describe("validateIconUrl (non-network branches)", () => { + it("rejects a non-URL string", async () => { + const res = await validateIconUrl("not a url"); + expect(res.valid).toBe(false); + }); + + it("rejects non-http protocols", async () => { + const res = await validateIconUrl("ftp://example.com/x.png"); + expect(res.valid).toBe(false); + }); +}); + +describe("normalizeUsername", () => { + it("lowercases and trims for case-insensitive uniqueness", () => { + expect(normalizeUsername(" Alice ")).toBe("alice"); + expect(normalizeUsername("BOB")).toBe("bob"); + }); +}); + +describe("requests_enabled global gate", () => { + beforeEach(async () => { + await resetDb(); + }); + + it("blocks request creation when disabled and allows when enabled", async () => { + const owner = await createUser(); + const agent = await createAgent(owner.id); + + await setRequestsEnabled(false); + const blocked = await createChangeRequest(agent, { + title: "t", + changes: sampleChanges, + }); + expect(blocked.ok).toBe(false); + if (!blocked.ok) expect(blocked.status).toBe(403); + + await setRequestsEnabled(true); + const allowed = await createChangeRequest(agent, { title: "t", changes: sampleChanges }); + expect(allowed.ok).toBe(true); + }); +}); diff --git a/Backend/src/tests/Signing.test.ts b/Backend/src/tests/Signing.test.ts new file mode 100644 index 0000000..2950b2e --- /dev/null +++ b/Backend/src/tests/Signing.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { ReceiptPayload, signReceipt, verifyReceipt } from "../lib/Signing"; + +const base: ReceiptPayload = { + request_id: "req-123", + decision: "APPROVED", + content_hash: "abc123", + approver_id: 7, + decided_at: "2026-01-01T00:00:00.000Z", +}; + +describe("Signing", () => { + it("produces a deterministic signature for the same payload", () => { + expect(signReceipt(base)).toBe(signReceipt({ ...base })); + }); + + it("verifies a genuine signature", () => { + expect(verifyReceipt(base, signReceipt(base))).toBe(true); + }); + + it("rejects a tampered content hash", () => { + const sig = signReceipt(base); + expect(verifyReceipt({ ...base, content_hash: "different" }, sig)).toBe(false); + }); + + it("rejects a decision swapped from APPROVED to REJECTED", () => { + const sig = signReceipt(base); + expect(verifyReceipt({ ...base, decision: "REJECTED" }, sig)).toBe(false); + }); + + it("rejects a signature bound to a different request", () => { + const sig = signReceipt(base); + expect(verifyReceipt({ ...base, request_id: "other" }, sig)).toBe(false); + }); + + it("rejects a malformed signature without throwing", () => { + expect(verifyReceipt(base, "not-hex")).toBe(false); + expect(verifyReceipt(base, "")).toBe(false); + }); +}); diff --git a/Backend/src/tests/SystemCrons.test.ts b/Backend/src/tests/SystemCrons.test.ts new file mode 100644 index 0000000..db6093e --- /dev/null +++ b/Backend/src/tests/SystemCrons.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { autoDeleteRequests, expireRequests } from "../lib/SystemCrons"; +import { prisma } from "../lib/db"; +import { createAgent, createUser, resetDb } from "./helpers"; + +async function makeRequest( + agentId: number, + userId: number, + opts: { state?: any; expiresAt?: Date; createdAt?: Date } = {}, +) { + return prisma.changeRequest.create({ + data: { + title: "r", + changes: [], + rawChanges: [], + contentHash: "h", + state: opts.state ?? "PENDING", + expiresAt: opts.expiresAt ?? new Date(Date.now() + 60000), + agentId, + userId, + ...(opts.createdAt ? { createdAt: opts.createdAt } : {}), + }, + }); +} + +describe("expireRequests cron", () => { + beforeEach(async () => { + await resetDb(); + }); + + it("expires only open requests past their deadline", async () => { + const user = await createUser(); + const agent = await createAgent(user.id); + const past = new Date(Date.now() - 1000); + const overduePending = await makeRequest(agent.id, user.id, { state: "PENDING", expiresAt: past }); + const overdueChanges = await makeRequest(agent.id, user.id, { + state: "CHANGES_REQUESTED", + expiresAt: past, + }); + const futurePending = await makeRequest(agent.id, user.id, { state: "PENDING" }); + const approvedPast = await makeRequest(agent.id, user.id, { state: "APPROVED", expiresAt: past }); + + const count = await expireRequests({ prisma }); + expect(count).toBe(2); + + expect((await prisma.changeRequest.findUnique({ where: { id: overduePending.id } }))!.state).toBe( + "EXPIRED", + ); + expect((await prisma.changeRequest.findUnique({ where: { id: overdueChanges.id } }))!.state).toBe( + "EXPIRED", + ); + expect((await prisma.changeRequest.findUnique({ where: { id: futurePending.id } }))!.state).toBe( + "PENDING", + ); + // Terminal states are never touched by the expiry sweep. + expect((await prisma.changeRequest.findUnique({ where: { id: approvedPast.id } }))!.state).toBe( + "APPROVED", + ); + }); + + it("creates a notification per expired request", async () => { + const user = await createUser(); + const agent = await createAgent(user.id); + await makeRequest(agent.id, user.id, { state: "PENDING", expiresAt: new Date(Date.now() - 1000) }); + await expireRequests({ prisma }); + const notifs = await prisma.notification.findMany({ where: { userId: user.id } }); + expect(notifs.some((n) => n.type === "REQUEST_EXPIRED")).toBe(true); + }); +}); + +describe("autoDeleteRequests cron", () => { + beforeEach(async () => { + await resetDb(); + }); + + it("deletes old requests only for opted-in users, respecting the window", async () => { + const optedIn = await prisma.user.update({ + where: { id: (await createUser()).id }, + data: { autoDeleteEnabled: true, autoDeleteDays: 7 }, + }); + const optedOut = await createUser(); + const agentIn = await createAgent(optedIn.id); + const agentOut = await createAgent(optedOut.id); + + const old = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000); + const recent = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000); + + const oldForOptedIn = await makeRequest(agentIn.id, optedIn.id, { + state: "CONSUMED", + createdAt: old, + }); + const recentForOptedIn = await makeRequest(agentIn.id, optedIn.id, { + state: "CONSUMED", + createdAt: recent, + }); + const oldForOptedOut = await makeRequest(agentOut.id, optedOut.id, { + state: "CONSUMED", + createdAt: old, + }); + + const deleted = await autoDeleteRequests({ prisma }); + expect(deleted).toBe(1); + + expect(await prisma.changeRequest.findUnique({ where: { id: oldForOptedIn.id } })).toBeNull(); + expect(await prisma.changeRequest.findUnique({ where: { id: recentForOptedIn.id } })).not.toBeNull(); + expect(await prisma.changeRequest.findUnique({ where: { id: oldForOptedOut.id } })).not.toBeNull(); + }); +}); diff --git a/Backend/src/tests/globalSetup.ts b/Backend/src/tests/globalSetup.ts new file mode 100644 index 0000000..04bfc76 --- /dev/null +++ b/Backend/src/tests/globalSetup.ts @@ -0,0 +1,18 @@ +import { execSync } from "child_process"; +import { join } from "path"; + +/** + * Vitest global setup — bring the test database schema up to date via Prisma + * migrations before any test runs. Runs once for the whole suite. + */ +export default function setup() { + const backendRoot = join(__dirname, "../.."); + const dbUrl = + process.env.TEST_DATABASE_URL || + "postgresql://patchpass:patchpass@localhost:5433/patchpass_test"; + execSync("npx prisma migrate deploy", { + cwd: backendRoot, + env: { ...process.env, DATABASE_URL: dbUrl }, + stdio: "inherit", + }); +} diff --git a/Backend/src/tests/helpers.ts b/Backend/src/tests/helpers.ts new file mode 100644 index 0000000..b67233d --- /dev/null +++ b/Backend/src/tests/helpers.ts @@ -0,0 +1,47 @@ +import { prisma } from "../lib/db"; +import { generateAgentApiKey } from "../lib/Authentication"; + +/** Wipe all data between tests. Order respects FK constraints (cascade handles rest). */ +export async function resetDb() { + await prisma.auditLog.deleteMany(); + await prisma.notification.deleteMany(); + await prisma.changeRequest.deleteMany(); + await prisma.agent.deleteMany(); + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.globalSetting.deleteMany(); +} + +let userCounter = 0; + +export async function createUser(overrides: Partial<{ username: string; role: "ADMIN" | "USER" }> = {}) { + userCounter += 1; + const username = overrides.username ?? `user${userCounter}_${Date.now()}`; + return prisma.user.create({ + data: { + username, + displayName: username, + password: "x", // not exercised in service-level tests + role: overrides.role ?? "USER", + }, + }); +} + +export async function createAgent( + ownerId: number, + overrides: Partial<{ name: string; maxPendingRequests: number; disabled: boolean }> = {}, +) { + return prisma.agent.create({ + data: { + name: overrides.name ?? "Test Agent", + apiKey: generateAgentApiKey(), + ownerId, + maxPendingRequests: overrides.maxPendingRequests ?? 5, + disabled: overrides.disabled ?? false, + }, + }); +} + +export const sampleChanges = [ + { type: "custom" as const, label: "Command", before: "a", after: "b" }, +]; diff --git a/Backend/tsconfig.json b/Backend/tsconfig.json new file mode 100644 index 0000000..cd8ed73 --- /dev/null +++ b/Backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "strict": true, + "module": "Node16", + "moduleResolution": "node16", + "lib": ["ES2022"], + "pretty": true, + "alwaysStrict": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "declaration": false, + "esModuleInterop": true, + "target": "ES2022", + "outDir": "dist", + "rootDir": "src", + "sourceMap": true, + "removeComments": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"] +} diff --git a/Backend/vitest.config.ts b/Backend/vitest.config.ts new file mode 100644 index 0000000..f046acd --- /dev/null +++ b/Backend/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "vitest/config"; +import { config as dotenvConfig } from "dotenv"; +import { join } from "path"; + +dotenvConfig({ path: join(__dirname, ".env") }); + +// Tests run against a dedicated database so they never touch dev data. +process.env.NODE_ENV = "test"; +process.env.DATABASE_URL = + process.env.TEST_DATABASE_URL || + "postgresql://patchpass:patchpass@localhost:5433/patchpass_test"; +// Deterministic secret so signature assertions are stable. +process.env.INSTANCE_SECRET = + process.env.INSTANCE_SECRET || "test_instance_secret_for_signing_receipts_0001"; +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "silent"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["src/tests/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + globalSetup: ["src/tests/globalSetup.ts"], + fileParallelism: false, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + }, + }, +}); diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f1699fd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,147 @@ +# PatchPass – Claude Code Guidelines + +## What is PatchPass? + +**A human approval layer for AI agents.** Agents submit structured change requests (code diffs, config updates, custom values); humans review and approve them in the browser; agents then verify the platform-signed decision through the API before applying anything. Packaged as a single `docker compose` stack. + +--- + +## Repo layout + +``` +Backend/ Node.js + TypeScript API (rjweb-server) + src/ + index.ts server bootstrap + static UI serving + notFound SPA fallback + lib/ service layer + shared utilities + ChangeRequestService.ts the request lifecycle — REST and MCP both call this + AgentService.ts agent CRUD (human-owned) + Authentication.ts session (human) + API key (agent) resolution + Signing.ts HMAC-SHA256 approval receipts + ChangeNormalization.ts diff normalization + content hashing + Limits.ts rate/pending/agent-count limits + Notifications.ts, WsHub.ts realtime notifications + SystemCrons.ts expiry + auto-delete jobs + DataManager.ts global (admin) settings + mcp/ MCP JSON-RPC server + tools + middlewares/, RouteAuth.ts + routes/ + api/ human-facing REST (auth, account, agents, change-requests, notifications, admin, global) + v1/ agent-facing REST (/v1/change-requests) + mcp.ts MCP endpoint (/mcp) + ws.ts WebSocket (/api/ws/notifications) + tests/ Vitest integration tests + prisma/schema.prisma +UI/ React + TypeScript + Tailwind (Vite), builds to UI/build +Dockerfile +docker-compose.yml +.gitea/workflows/deploy.yml +``` + +--- + +## Stack + +| Layer | Technology | +|---|---| +| Backend | Node.js + TypeScript (strict), rjweb-server | +| ORM | Prisma 7 (`@prisma/adapter-pg`) | +| Database | PostgreSQL | +| Frontend | React + Tailwind CSS (Vite) | +| Package manager | pnpm (Backend and UI are separate pnpm workspaces) | +| Backend tests | Vitest | + +--- + +## Development workflow + +### Branch strategy + +``` +feature/ → dev → main +``` + +### Running locally + +```bash +# Backend +cd Backend +pnpm dev # esbuild + node, http://localhost:5000 + +# UI (separate terminal) +cd UI +pnpm dev # vite on :3000, proxies /api and /v1 to :5000 +``` + +### Before committing + +1. **Backend lint:** `cd Backend && pnpm lint` +2. **Backend tests:** `cd Backend && pnpm test` +3. **UI lint:** `cd UI && pnpm lint` +4. **UI build check:** `cd UI && pnpm build` + +Never commit code that fails lint or tests. + +--- + +## Key rules + +### The service layer is the single source of truth +`ChangeRequestService.ts` contains ALL request lifecycle logic and authorization. The REST routes (`routes/v1`, `routes/api`) and the MCP tools (`lib/mcp/tools`) are thin adapters that call it. **Do not duplicate authorization or business logic** in a route or tool — add it to the service. + +### Request state machine +States: `PENDING`, `CHANGES_REQUESTED`, `APPROVED`, `REJECTED`, `EXPIRED`, `CONSUMED`, `CANCELLED`. +- Decisions are only allowed on `PENDING`. +- Agent updates are only allowed on `PENDING` / `CHANGES_REQUESTED`. +- `REQUEST_CHANGES` requires a comment (≤500 chars) and resets to `PENDING` on agent resubmit (`resubmitted=true`). +- No mutation after `APPROVED`, `REJECTED`, `EXPIRED`, `CONSUMED`, or `CANCELLED`. +- Approvals are single-use: `consume` moves `APPROVED → CONSUMED`. + +### Signing +Approve/reject decisions are signed with HMAC-SHA256 over a fixed canonical field order (see `Signing.ts`). The signature binds the decision to `content_hash`. Never reorder the canonical fields — it breaks every existing signature. + +### Content hashing +`computeContentHash` hashes `{title, description, normalizedChanges}` with key-sorted canonical JSON and LF-normalized diffs. Equivalent inputs (CRLF vs LF, key order) must hash identically; any semantic change must change the hash. Covered by tests. + +### Limits +`Limits.ts` centralizes: 15 requests/agent/hour, 5 agents/human, 1–10 pending/agent (default 5), expiry 60s–12h (default 30 min), auto-delete ≥7 days. Enforce through these helpers, not ad hoc. + +### Auth +- Humans: session cookie (`patchpass_session`), bcrypt passwords, optional TOTP 2FA. +- Agents: one API key each, `x-api-key` header. +Resolution is in `Authentication.ts`; routes use `requireSession` / `requireAgent` / `requireAdmin` from `RouteAuth.ts`. Disabled humans/agents are rejected. + +### Admin +First registered user is `ADMIN`. Global settings (`registration_enabled`, `requests_enabled`) live in `DataManager.ts`. Admin viewing another user's request payload is explicit and audited (`recordAudit`). Never remove the last admin. + +### UTC & pagination +All timestamps are UTC ISO strings at the API boundary. History and admin logs are paginated. + +--- + +## Database migrations – MANDATORY rules + +> **Never use `prisma db push` or `prisma db pull` to evolve the schema.** + +When you change `prisma/schema.prisma`: + +1. Create a migration: `cd Backend && pnpm migrate` (`prisma migrate dev`), give a short slug. +2. Commit the generated `prisma/migrations/_/migration.sql` with the schema change. +3. Never hand-edit an applied `migration.sql` — create a new migration. +4. Production/CI: `pnpm migrate:deploy`. + +Prisma 7: the datasource URL lives in `prisma.config.ts` via `env("DATABASE_URL")`, not in `schema.prisma`. The client uses the `PrismaPg` adapter — do not remove it. Run `pnpm generate` for TS types without a migration. + +--- + +## General coding rules + +- TypeScript strict mode is on. Avoid `any` without reason (it is a lint warning). +- Do not use `console.*` in the Backend. Use `createLogger(component)` from `lib/logger.ts`. +- UI state uses React built-ins (Context + hooks) — no external state library. +- Follow existing conventions: route handlers in `routes/`, shared logic in `lib/`, MCP tools in `lib/mcp/tools/`. + +--- + +## Keeping CLAUDE.md and AGENTS.md in sync – MANDATORY + +`CLAUDE.md` (Claude Code) and `AGENTS.md` (all other AI agents) must always reflect the same rules. Any rule added, removed, or amended in one must be mirrored in the other **in the same commit**. If they drift, fix the gap before continuing. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..342ab19 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM node:24-bookworm-slim AS build + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable && corepack prepare pnpm@11.5.2 --activate +RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY Backend/package.json Backend/pnpm-workspace.yaml ./Backend/ +COPY UI/package.json UI/pnpm-workspace.yaml ./UI/ + +# Install in separate layers to keep peak memory low. +RUN cd Backend && pnpm install +RUN cd UI && pnpm install + +COPY . . + +# Build the frontend, then generate the Prisma client and compile the backend. +RUN cd UI && pnpm run build +RUN cd Backend && DATABASE_URL=postgresql://x:x@localhost/x pnpm run generate && pnpm run build + +FROM node:24-bookworm-slim AS runtime + +ENV NODE_ENV=production +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable && corepack prepare pnpm@11.5.2 --activate +RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY Backend/package.json Backend/pnpm-workspace.yaml Backend/prisma.config.ts ./Backend/ +COPY Backend/prisma ./Backend/prisma + +RUN cd Backend && pnpm install --prod && DATABASE_URL=postgresql://x:x@localhost/x pnpm run generate + +COPY --from=build /app/Backend/dist ./Backend/dist +COPY --from=build /app/UI/build ./UI/build + +EXPOSE 5000 + +# Apply migrations on boot, then start the API (which also serves the UI build). +CMD ["sh", "-c", "cd /app/Backend && pnpm migrate:deploy && cd dist && node index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f014ba8 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# PatchPass + +**A human approval layer for AI agents.** + +> Review changes. Approve intent. Let agents proceed. + +Agents submit structured change requests containing code diffs, configuration updates, or custom values. Humans review and approve them in the browser, then agents verify the platform-signed decision through the API before applying anything. + +--- + +## How it works + +``` +Agent PatchPass Human + │ create_request ───────────► │ │ + │ (diffs/config/custom) │ normalize · hash · store │ + │ ◄─── request_id + URL ────────│ ──── notify (WS) ──────────► │ reviews in browser + │ │ │ APPROVE / REJECT / REQUEST_CHANGES + │ get_request ──────────────► │ ◄──── signed decision ──────│ + │ ◄─── state + receipt ─────────│ │ + │ consume_approval ─────────► │ mark CONSUMED (single-use) │ + │ ◄─── receipt (HMAC) ──────────│ │ + │ apply changes │ │ +``` + +- **Decisions are platform-signed.** Approvals and rejections carry an HMAC-SHA256 receipt bound to the exact reviewed content (`content_hash`). If the agent changes anything after approval, the receipt no longer matches. +- **Approvals are single-use.** An agent must `consume` an approval before proceeding; it cannot be replayed. +- **Two ways in for agents:** a REST API (`/v1/...`) and an MCP server (`/mcp`), both authenticated with a per-agent API key and backed by the same service layer. + +## Request states + +`PENDING` · `CHANGES_REQUESTED` · `APPROVED` · `REJECTED` · `EXPIRED` · `CONSUMED` · `CANCELLED` + +No update is possible after approval, rejection, expiry, consumption, or cancellation. A `REQUEST_CHANGES` decision requires a comment and returns the request to the agent, which can `update_request` to resubmit (highlighted in the UI as **UPDATED**). + +## Limits + +| Limit | Value | +|---|---| +| Requests per agent per hour | 15 | +| Agents per human | 5 | +| Simultaneous pending requests per agent | 1–10 (human-configured, default 5) | +| Request expiry | default 30 min, max 12 h | + +## Tech stack + +| Layer | Technology | +|---|---| +| Backend | Node.js + TypeScript, [rjweb-server](https://server.rjweb.dev) | +| ORM | Prisma 7 (`@prisma/adapter-pg`) | +| Database | PostgreSQL | +| Frontend | React + TypeScript + Tailwind CSS (Vite), served by the backend | +| Realtime | rjweb WebSocket channels | +| Tests | Vitest | + +## Repository layout + +``` +Backend/ Node + TypeScript API (rjweb-server) + Prisma + src/ + index.ts server bootstrap + static UI serving + lib/ service layer, auth, signing, limits, crons, MCP tools + routes/ REST (/api, /v1), MCP (/mcp), WebSocket (/api/ws) + tests/ Vitest integration tests + prisma/schema.prisma +UI/ React + Tailwind frontend (builds to UI/build) +Dockerfile multi-stage build (UI + backend) +docker-compose.yml backend + postgres +.gitea/workflows/ CI: build · lint · test · push image +``` + +## Running locally + +Prerequisites: Node 24+, pnpm, and Postgres (or Docker). + +```bash +# 1. Start Postgres (Docker) +docker run -d --name patchpass-db \ + -e POSTGRES_USER=patchpass -e POSTGRES_PASSWORD=patchpass -e POSTGRES_DB=patchpass \ + -p 5433:5432 postgres:16-alpine + +# 2. Backend +cd Backend +cp ../example.env .env # then edit DATABASE_URL / INSTANCE_SECRET +pnpm install +pnpm generate +pnpm migrate # apply migrations +pnpm dev # http://localhost:5000 + +# 3. UI (separate terminal, for hot-reload dev) +cd UI +pnpm install +pnpm dev # http://localhost:3000 (proxies /api to :5000) +``` + +For a production-style run, `cd UI && pnpm build` — the backend then serves `UI/build` at `/`. + +### Tests + +```bash +cd Backend +pnpm test # spins up against the test database (see vitest.config.ts) +``` + +## Deploying + +```bash +cp example.env .env # set DATABASE_URL=...@db:5432/..., a strong INSTANCE_SECRET +docker compose up -d --build +``` + +The backend applies migrations on boot and serves the UI. The first account to register becomes the **admin**. + +## Connecting an agent + +Create an agent in the UI, then use its API key. + +**OpenClaw** — add to `~/.openclaw/openclaw.json`: + +```json +{ + "mcp": { + "servers": { + "patchpass": { + "transport": "streamable-http", + "url": "https://your-host/mcp", + "headers": { "x-api-key": "pp_agent_..." } + } + } + } +} +``` + +**REST**: + +```bash +curl -X POST https://your-host/v1/change-requests \ + -H "x-api-key: pp_agent_..." -H "Content-Type: application/json" \ + -d '{"title":"Bump timeout","changes":[{"type":"config","path":"timeout","before":30,"after":60,"content_type":"integer"}]}' +``` + +Tell your agent: *before taking any consequential action, call `create_request`, wait for approval, then `consume_approval` before proceeding.* + +## Privacy & data + +Humans can export all their data as JSON and delete their account (cascading to agents, requests, and notifications) from **Settings**. Agent icon URLs are fetched once for validation and never stored. See the in-app Privacy Policy and Terms of Service. + +## License + +MIT-0 diff --git a/UI/.eslintrc.cjs b/UI/.eslintrc.cjs new file mode 100644 index 0000000..9725936 --- /dev/null +++ b/UI/.eslintrc.cjs @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { browser: true, es2022: true }, + parser: "@typescript-eslint/parser", + parserOptions: { ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: true } }, + plugins: ["@typescript-eslint", "react-hooks"], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + settings: {}, + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "no-empty": ["error", { allowEmptyCatch: true }], + }, + ignorePatterns: ["build/", "node_modules/", "vite.config.ts"], +}; diff --git a/UI/index.html b/UI/index.html new file mode 100644 index 0000000..0593d03 --- /dev/null +++ b/UI/index.html @@ -0,0 +1,14 @@ + + + + + + + + PatchPass + + +
+ + + diff --git a/UI/package.json b/UI/package.json new file mode 100644 index 0000000..50f7e08 --- /dev/null +++ b/UI/package.json @@ -0,0 +1,32 @@ +{ + "name": "patchpass-ui", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "tailwind:build": "echo \"tailwind is bundled by vite\"", + "preview": "vite preview", + "lint": "eslint src --ext .ts,.tsx" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.0", + "react-toastify": "^10.0.6" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.14", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^5.1.0", + "tailwindcss": "^4.1.14", + "typescript": "^5.9.3", + "vite": "^6.0.7" + } +} diff --git a/UI/pnpm-lock.yaml b/UI/pnpm-lock.yaml new file mode 100644 index 0000000..aab1558 --- /dev/null +++ b/UI/pnpm-lock.yaml @@ -0,0 +1,2503 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.30.0 + version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-toastify: + specifier: ^10.0.6 + version: 10.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.14 + version: 4.3.3(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)) + '@types/react': + specifier: ^18.3.12 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.31) + '@typescript-eslint/eslint-plugin': + specifier: ^8.20.0 + version: 8.64.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.20.0 + version: 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: ^5.1.0 + version: 5.2.0(eslint@8.57.1) + tailwindcss: + specifier: ^4.1.14 + version: 4.3.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^6.0.7 + version: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@remix-run/router@1.23.3': + resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@typescript-eslint/eslint-plugin@8.64.0': + resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.64.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.64.0': + resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.64.0': + resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.64.0': + resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.64.0': + resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.43: + resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.6: + resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + electron-to-chromium@1.5.393: + resolution: {integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==} + + enhanced-resolve@5.24.2: + resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} + engines: {node: '>=10.13.0'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + 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 + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.30.4: + resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.4: + resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react-toastify@10.0.6: + resolution: {integrity: sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.6 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@remix-run/router@1.23.3': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.2 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/estree@1.0.9': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/type-utils': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.64.0 + eslint: 8.57.1 + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.64.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.64.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + + '@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.64.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.64.0': {} + + '@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.64.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.64.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.3': {} + + '@vitejs/plugin-react@4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.43: {} + + brace-expansion@1.1.16: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.6: + dependencies: + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.393 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.6) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001806: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + electron-to-chromium@1.5.393: {} + + enhanced-resolve@5.24.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.3 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.3.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.2: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.16 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.51: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.4(react@18.3.1) + + react-router@6.30.4(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + + react-toastify@10.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + text-table@0.2.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.6): + dependencies: + browserslist: 4.28.6 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.19 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/UI/pnpm-workspace.yaml b/UI/pnpm-workspace.yaml new file mode 100644 index 0000000..660d3e7 --- /dev/null +++ b/UI/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@tailwindcss/oxide': true + esbuild: true diff --git a/UI/src/api/client.ts b/UI/src/api/client.ts new file mode 100644 index 0000000..47d8885 --- /dev/null +++ b/UI/src/api/client.ts @@ -0,0 +1,150 @@ +import type { + Agent, + AdminUser, + AuditLog, + ChangeRequest, + GlobalSettings, + Notification, + RequestState, + User, +} from "./types"; + +export class ApiError extends Error { + status: number; + data?: unknown; + constructor(message: string, status: number, data?: unknown) { + super(message); + this.status = status; + this.data = data; + } +} + +async function api(path: string, opts: RequestInit = {}): Promise { + const res = await fetch(path, { + credentials: "include", + headers: + opts.body && !(opts.headers && "Content-Type" in opts.headers) + ? { "Content-Type": "application/json", ...(opts.headers || {}) } + : opts.headers, + ...opts, + }); + let body: any = null; + try { + body = await res.json(); + } catch { + /* no body */ + } + if (!res.ok || body?.status === "FAILED") { + throw new ApiError(body?.message || `Request failed (${res.status})`, res.status, body?.data); + } + return (body?.data ?? body) as T; +} + +// ── Auth ────────────────────────────────────────────────────────────────────── +export const auth = { + me: () => api("/api/auth/me"), + login: (username: string, password: string, totp?: string) => + api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password, totp }) }), + register: (username: string, display_name: string, password: string) => + api("/api/auth/register", { + method: "POST", + body: JSON.stringify({ username, display_name, password }), + }), + logout: () => api("/api/auth/logout", { method: "POST" }), + setup2fa: () => api<{ secret: string; otpauth_url: string }>("/api/auth/2fa/setup", { method: "POST" }), + enable2fa: (totp: string) => api("/api/auth/2fa/enable", { method: "POST", body: JSON.stringify({ totp }) }), + disable2fa: (password: string) => + api("/api/auth/2fa/disable", { method: "POST", body: JSON.stringify({ password }) }), +}; + +// ── Account ──────────────────────────────────────────────────────────────────── +export const account = { + updateProfile: (display_name: string) => + api("/api/account/profile", { method: "PATCH", body: JSON.stringify({ display_name }) }), + changePassword: (current_password: string, new_password: string) => + api("/api/account/password", { + method: "POST", + body: JSON.stringify({ current_password, new_password }), + }), + updateSettings: (settings: { auto_delete_enabled?: boolean; auto_delete_days?: number }) => + api<{ auto_delete_enabled: boolean; auto_delete_days: number }>("/api/account/settings", { + method: "PATCH", + body: JSON.stringify(settings), + }), + deleteAccount: (password: string) => + api("/api/account", { method: "DELETE", body: JSON.stringify({ password, confirm: "DELETE" }) }), + exportUrl: "/api/account/export", +}; + +// ── Agents ───────────────────────────────────────────────────────────────────── +export const agents = { + list: () => api("/api/agents"), + create: (input: Partial) => api("/api/agents", { method: "POST", body: JSON.stringify(input) }), + update: (id: number, input: Partial) => + api(`/api/agents/${id}`, { method: "PATCH", body: JSON.stringify(input) }), + regenerateKey: (id: number) => api(`/api/agents/${id}/regenerate-key`, { method: "POST" }), + setDisabled: (id: number, disabled: boolean) => + api(`/api/agents/${id}/disabled`, { method: "POST", body: JSON.stringify({ disabled }) }), + remove: (id: number) => api(`/api/agents/${id}`, { method: "DELETE" }), +}; + +// ── Change requests (human) ────────────────────────────────────────────────────── +export const requests = { + list: (params: { page?: number; page_size?: number; state?: string; agent_id?: number } = {}) => { + const q = new URLSearchParams(); + if (params.page) q.set("page", String(params.page)); + if (params.page_size) q.set("page_size", String(params.page_size)); + if (params.state) q.set("state", params.state); + if (params.agent_id) q.set("agent_id", String(params.agent_id)); + return api<{ page: number; page_size: number; total: number; requests: ChangeRequest[] }>( + `/api/change-requests?${q.toString()}`, + ); + }, + summary: () => api<{ counts: Record }>("/api/change-requests/summary"), + get: (id: string) => api(`/api/change-requests/${id}`), + decide: (id: string, decision: "APPROVE" | "REJECT" | "REQUEST_CHANGES", comment?: string) => + api(`/api/change-requests/${id}/decision`, { + method: "POST", + body: JSON.stringify({ decision, comment }), + }), +}; + +// ── Notifications ──────────────────────────────────────────────────────────────── +export const notifications = { + list: (params: { page?: number; page_size?: number; unread?: boolean } = {}) => { + const q = new URLSearchParams(); + if (params.page) q.set("page", String(params.page)); + if (params.page_size) q.set("page_size", String(params.page_size)); + if (params.unread) q.set("unread", "true"); + return api<{ total: number; unread_count: number; notifications: Notification[] }>( + `/api/notifications?${q.toString()}`, + ); + }, + markRead: (id: number) => api(`/api/notifications/${id}/read`, { method: "POST" }), + markAllRead: () => api("/api/notifications/read-all", { method: "POST" }), + clear: () => api("/api/notifications", { method: "DELETE" }), +}; + +// ── Global ─────────────────────────────────────────────────────────────────────── +export const global = { + get: () => api("/api/global"), +}; + +// ── Admin ───────────────────────────────────────────────────────────────────────── +export const admin = { + users: () => api("/api/admin/users"), + updateUser: (id: number, input: { role?: "ADMIN" | "USER"; disabled?: boolean }) => + api(`/api/admin/users/${id}`, { method: "PATCH", body: JSON.stringify(input) }), + deleteUser: (id: number) => api(`/api/admin/users/${id}`, { method: "DELETE" }), + settings: () => api("/api/admin/settings"), + updateSettings: (input: { registration_enabled?: boolean; requests_enabled?: boolean }) => + api("/api/admin/settings", { method: "PATCH", body: JSON.stringify(input) }), + agents: () => api<(Agent & { owner: { id: number; username: string } })[]>("/api/admin/agents"), + setAgentDisabled: (id: number, disabled: boolean) => + api(`/api/admin/agents/${id}/disabled`, { method: "POST", body: JSON.stringify({ disabled }) }), + deleteAgent: (id: number) => api(`/api/admin/agents/${id}`, { method: "DELETE" }), + auditLogs: (page = 1) => + api<{ total: number; page: number; page_size: number; logs: AuditLog[] }>( + `/api/admin/audit-logs?page=${page}`, + ), +}; diff --git a/UI/src/api/types.ts b/UI/src/api/types.ts new file mode 100644 index 0000000..29f18bf --- /dev/null +++ b/UI/src/api/types.ts @@ -0,0 +1,125 @@ +export type RequestState = + | "PENDING" + | "CHANGES_REQUESTED" + | "APPROVED" + | "REJECTED" + | "EXPIRED" + | "CONSUMED" + | "CANCELLED"; + +export type User = { + id: number; + username: string; + display_name: string; + role: "ADMIN" | "USER"; + totp_enabled: boolean; + auto_delete_enabled: boolean; + auto_delete_days: number; +}; + +export type Agent = { + id: number; + name: string; + description: string | null; + website: string | null; + icon_url: string | null; + disabled: boolean; + max_pending_requests: number; + created_at: string; + updated_at: string; + api_key?: string; + api_key_masked?: string; + pending_count?: number; +}; + +export type UnifiedDiffChange = { type: "unified_diff"; path: string; content: string }; +export type ConfigChange = { + type: "config"; + path: string; + before?: unknown; + after?: unknown; + content_type?: string | null; +}; +export type CustomChange = { type: "custom"; label: string; before?: unknown; after?: unknown }; +export type Change = UnifiedDiffChange | ConfigChange | CustomChange; + +export type Receipt = { + payload: { + request_id: string; + decision: "APPROVED" | "REJECTED"; + content_hash: string; + approver_id: number; + decided_at: string; + }; + signature: string; + algorithm: string; + consumed: boolean; + consumed_at: string | null; +}; + +export type ChangeRequest = { + request_id: string; + title: string; + description: string | null; + changes: Change[]; + metadata: Record | null; + content_hash: string; + state: RequestState; + comment: string | null; + expires_at: string; + created_at: string; + updated_at: string; + decided_at: string | null; + consumed_at: string | null; + cancelled_at: string | null; + update_count: number; + resubmitted: boolean; + approval_url: string; + receipt: Receipt | null; + agent?: { + id: number; + name: string; + description: string | null; + website: string | null; + icon_url: string | null; + disabled: boolean; + } | null; +}; + +export type Notification = { + id: number; + type: string; + title: string; + message: string; + request_id: string | null; + read: boolean; + created_at: string; +}; + +export type GlobalSettings = { + version?: string; + registration_enabled: boolean; + requests_enabled: boolean; +}; + +export type AdminUser = { + id: number; + username: string; + display_name: string; + role: "ADMIN" | "USER"; + disabled: boolean; + totp_enabled: boolean; + created_at: string; + agent_count: number; + request_count: number; +}; + +export type AuditLog = { + id: number; + action: string; + detail: string | null; + target_type: string | null; + target_id: string | null; + actor: { id: number; username: string } | null; + created_at: string; +}; diff --git a/UI/src/components/AgentAvatar.tsx b/UI/src/components/AgentAvatar.tsx new file mode 100644 index 0000000..ab4952e --- /dev/null +++ b/UI/src/components/AgentAvatar.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; + +export function AgentAvatar({ + name, + iconUrl, + size = 36, +}: { + name: string; + iconUrl?: string | null; + size?: number; +}) { + const [errored, setErrored] = useState(false); + const initials = name + .split(/\s+/) + .slice(0, 2) + .map((w) => w[0]?.toUpperCase() ?? "") + .join(""); + + if (iconUrl && !errored) { + return ( + {name} setErrored(true)} + className="rounded-lg border border-border object-cover" + style={{ width: size, height: size }} + /> + ); + } + return ( +
+ {initials || "?"} +
+ ); +} diff --git a/UI/src/components/AgentModals.tsx b/UI/src/components/AgentModals.tsx new file mode 100644 index 0000000..2ffe24b --- /dev/null +++ b/UI/src/components/AgentModals.tsx @@ -0,0 +1,250 @@ +import { useState } from "react"; +import { toast } from "react-toastify"; +import { agents as agentsApi } from "../api/client"; +import type { Agent } from "../api/types"; +import { Modal } from "./Modal"; +import { Button, Input, Textarea } from "./ui"; +import { CodeBlock } from "./CodeBlock"; + +// ── Create / edit form ─────────────────────────────────────────────────────────── + +export function AgentFormModal({ + open, + onClose, + agent, + onSaved, +}: { + open: boolean; + onClose: () => void; + agent?: Agent | null; + onSaved: (agent: Agent, created: boolean) => void; +}) { + const editing = !!agent; + const [name, setName] = useState(agent?.name ?? ""); + const [description, setDescription] = useState(agent?.description ?? ""); + const [website, setWebsite] = useState(agent?.website ?? ""); + const [iconUrl, setIconUrl] = useState(agent?.icon_url ?? ""); + const [maxPending, setMaxPending] = useState(agent?.max_pending_requests ?? 5); + const [loading, setLoading] = useState(false); + + const submit = async () => { + if (!name.trim()) return toast.error("Name is required"); + setLoading(true); + try { + const payload = { + name: name.trim(), + description: description.trim() || null, + website: website.trim() || null, + icon_url: iconUrl.trim() || null, + max_pending_requests: maxPending, + }; + const saved = editing + ? await agentsApi.update(agent!.id, payload) + : await agentsApi.create(payload); + toast.success(editing ? "Agent updated" : "Agent created"); + onSaved(saved, !editing); + onClose(); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed"); + } finally { + setLoading(false); + } + }; + + return ( + + + + + } + > +
+ setName(e.target.value)} autoFocus /> +