From a103ab3014804667dc2bfa515d4d7329d08c637d Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:23:15 -0700 Subject: [PATCH 1/5] feat(agent-004): implement full validator monitor workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the AGENT-002 / AGENT-003 structure to give AGENT-004 Validator Monitor the same standalone TypeScript implementation: - `agent-004-validator-monitor/` — standalone Node.js process - `src/index.ts` — banner, cycle runner, polling vs single-run mode - `src/config.ts` — config + thresholds mirrored from the character - `src/types.ts` — staking / slashing / gov types + per-workflow shapes - `src/ledger.ts` — LCD client for staking, slashing, gov endpoints - `src/store.ts` — SQLite state (token snapshots, commission history, scorecards, decentralization snapshots, workflow executions) - `src/ooda.ts` — generic OODA executor (same shape as agent-002/003) - `src/monitor.ts` — Claude narrative layer, one function per workflow - `src/output.ts` — console + optional Discord webhook dispatcher - `src/workflows/performance-tracking.ts` — WF-VM-01 - `src/workflows/delegation-flow-analysis.ts` — WF-VM-02 - `src/workflows/decentralization-monitor.ts` — WF-VM-03 Scoring (M014): - Uptime component: signed_blocks_window − missed_blocks_counter, weighted at config.validator.scoreWeightUptime (default 400) - Governance component: votes cast / recent finalized proposals, weighted at config.validator.scoreWeightGovernance (default 350) - Stability component: full weight minus penalties for jailing (100) and commission changes in the trailing window (40 each), floored at 0, weighted at config.validator.scoreWeightStability (default 250) - Composite = sum, 0..1000, PoA eligible when >= 800 Decentralization (WF-VM-03): - Nakamoto coefficient uses the 33.4% halt-threshold convention - Gini index uses the textbook formula on token amounts normalized to uregen - Health classification from Nakamoto floor (<=5 CRITICAL, <=8 WARNING), Gini ceiling (>=0.65 WARNING), and single-validator concentration (>=33% CRITICAL, >=20% WARNING) Delegation flows (WF-VM-02): - Snapshot each validator's `tokens` field every cycle - Derive flows from the delta against the previous snapshot - Whale-sized movements (>= 100,000 REGEN by default) tagged in the summary and raise the alert level Determinism: all scoring, Nakamoto, Gini, health, and whale detection are computed in plain TypeScript. Claude is only used for the narrative layer. Keeps the agent cheap, reproducible, and auditable — and makes the hard parts trivially unit-testable in a follow-up PR. Deliberate MVP proxies (documented in the agent README): 1. Token-delta as the delegation source for WF-VM-02 until a real MsgDelegate / MsgUndelegate / MsgRedelegate tx-stream client lands. 2. Governance participation currently scores 0 for every validator because the operator→delegator bech32 conversion is deferred to a follow-up PR. Relative composites still surface real differences in uptime and stability without asymmetric penalty. 3. MVP signing-info join on the raw consensus key — falls back to assuming 100% uptime when no match is found, which under-counts real issues rather than smearing a healthy validator. CI: the new agent is added to the `agents` job so `npx tsc --noEmit` runs against it on every PR, matching the existing agent-002-governance-analyst wiring. - Lands in: `agent-004-validator-monitor/`, `.github/workflows/ci.yml` - Changes: new standalone AGENT-004 process with 3 workflows (WF-VM-01/02/03) - Validate: `cd agent-004-validator-monitor && npm ci && npx tsc --noEmit` Refs phase-2/2.2-agentic-workflows.md §WF-VM-01, §WF-VM-02, §WF-VM-03 Refs agents/packages/agents/src/characters/validator-monitor.ts (#64) --- .github/workflows/ci.yml | 8 + agent-004-validator-monitor/.env.example | 14 + agent-004-validator-monitor/.gitignore | 4 + agent-004-validator-monitor/README.md | 116 ++ agent-004-validator-monitor/package-lock.json | 1475 +++++++++++++++++ agent-004-validator-monitor/package.json | 27 + agent-004-validator-monitor/src/config.ts | 62 + agent-004-validator-monitor/src/index.ts | 102 ++ agent-004-validator-monitor/src/ledger.ts | 151 ++ agent-004-validator-monitor/src/monitor.ts | 201 +++ agent-004-validator-monitor/src/ooda.ts | 83 + agent-004-validator-monitor/src/output.ts | 58 + agent-004-validator-monitor/src/store.ts | 300 ++++ agent-004-validator-monitor/src/types.ts | 170 ++ .../src/workflows/decentralization-monitor.ts | 223 +++ .../src/workflows/delegation-flow-analysis.ts | 182 ++ .../src/workflows/performance-tracking.ts | 305 ++++ agent-004-validator-monitor/tsconfig.json | 20 + 18 files changed, 3501 insertions(+) create mode 100644 agent-004-validator-monitor/.env.example create mode 100644 agent-004-validator-monitor/.gitignore create mode 100644 agent-004-validator-monitor/README.md create mode 100644 agent-004-validator-monitor/package-lock.json create mode 100644 agent-004-validator-monitor/package.json create mode 100644 agent-004-validator-monitor/src/config.ts create mode 100644 agent-004-validator-monitor/src/index.ts create mode 100644 agent-004-validator-monitor/src/ledger.ts create mode 100644 agent-004-validator-monitor/src/monitor.ts create mode 100644 agent-004-validator-monitor/src/ooda.ts create mode 100644 agent-004-validator-monitor/src/output.ts create mode 100644 agent-004-validator-monitor/src/store.ts create mode 100644 agent-004-validator-monitor/src/types.ts create mode 100644 agent-004-validator-monitor/src/workflows/decentralization-monitor.ts create mode 100644 agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts create mode 100644 agent-004-validator-monitor/src/workflows/performance-tracking.ts create mode 100644 agent-004-validator-monitor/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c30e..acf7ad6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,14 @@ jobs: working-directory: agent-002-governance-analyst run: npx tsc --noEmit + - name: Install agent-004-validator-monitor/ dependencies + working-directory: agent-004-validator-monitor + run: npm ci + + - name: Typecheck agent-004-validator-monitor/ + working-directory: agent-004-validator-monitor + run: npx tsc --noEmit + # ── Spec verification: schemas, reference impls, datasets ─────────── verify-specs: name: Verify Specs diff --git a/agent-004-validator-monitor/.env.example b/agent-004-validator-monitor/.env.example new file mode 100644 index 0000000..c052d2b --- /dev/null +++ b/agent-004-validator-monitor/.env.example @@ -0,0 +1,14 @@ +# Required: Anthropic API key for Claude-powered narrative reports +ANTHROPIC_API_KEY=sk-ant-... + +# Regen LCD endpoint (defaults to public endpoint if not set) +REGEN_LCD_URL=https://regen.api.chandrastation.com + +# Optional: Discord webhook for posting validator reports and alerts +DISCORD_WEBHOOK_URL= + +# Optional: polling interval in seconds (default: 900 = 15 minutes) +POLL_INTERVAL_SECONDS=900 + +# Optional: Claude model to use (default: claude-sonnet-4-5-20250929) +ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 diff --git a/agent-004-validator-monitor/.gitignore b/agent-004-validator-monitor/.gitignore new file mode 100644 index 0000000..c44c9e2 --- /dev/null +++ b/agent-004-validator-monitor/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.db +.env diff --git a/agent-004-validator-monitor/README.md b/agent-004-validator-monitor/README.md new file mode 100644 index 0000000..4a3dc1e --- /dev/null +++ b/agent-004-validator-monitor/README.md @@ -0,0 +1,116 @@ +# AGENT-004: Regen Validator Monitor + +**Layer 1 (fully automated, read-only, informational) agent that watches the Regen Network validator set, scores per-validator performance against the M014 methodology, tracks delegation flows, and monitors network decentralization.** + +Mirrors the AGENT-002 Governance Analyst / AGENT-003 Market Monitor structure: the same OODA executor, the same standalone Node.js process shape, the same SQLite-backed local state. Scope is validator infrastructure intelligence rather than governance or marketplace. + +## What it does + +| Workflow | Trigger | Output | +|----------|---------|--------| +| **WF-VM-01** Performance Tracking | Periodic (each poll cycle) | Per-validator scorecards, composite 0-1000, PoA eligibility, performance alerts | +| **WF-VM-02** Delegation Flow Analysis | Periodic (delta against previous snapshot) | Net inflow/outflow, whale detection, top movers | +| **WF-VM-03** Decentralization Monitoring | Periodic (each poll cycle) | Nakamoto coefficient, Gini index, top-N share, health tier, trend vs previous | + +Each workflow is an **OODA loop** (Observe → Orient → Decide → Act). Numeric decisions (scoring, Nakamoto, Gini, health tier, whale detection) are computed **deterministically**; Claude is only used for the narrative layer. + +## Architecture + +``` +Regen Ledger (LCD REST API) + ↓ observe +AGENT-004 (OODA engine) + ↓ orient (deterministic) + ↓ decide (deterministic) +Local SQLite (state) + ↓ act (narrative via Claude) +Console / Discord webhook +``` + +**No MCP dependency.** Talks directly to any Cosmos LCD endpoint. When Ledger MCP becomes available, the `LedgerClient` can be swapped behind the same interface. + +**No ElizaOS dependency.** Standalone Node.js process. The ElizaOS character for AGENT-004 lives at `agents/packages/agents/src/characters/validator-monitor.ts`; this package shares the same system prompt text and the same threshold constants so downstream tooling has a single source of truth. + +## Quick start + +```bash +# 1. Install +cd agent-004-validator-monitor +npm install + +# 2. Configure +cp .env.example .env +# Edit .env — at minimum set ANTHROPIC_API_KEY + +# 3. Run (single cycle) +npm run analyze + +# 4. Run (continuous polling) +npm start + +# 5. Run (dev mode with auto-reload) +npm run dev +``` + +## Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `ANTHROPIC_API_KEY` | Yes | — | Claude API key | +| `REGEN_LCD_URL` | No | `https://regen.api.chandrastation.com` | Cosmos LCD endpoint | +| `DISCORD_WEBHOOK_URL` | No | — | Discord webhook for posting reports | +| `POLL_INTERVAL_SECONDS` | No | `900` (15 min) | Polling interval (seconds) | +| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-5-20250929` | Claude model to use | + +Thresholds live in `src/config.ts` under `validator.*` and mirror the character definition at `agents/packages/agents/src/characters/validator-monitor.ts`. + +## How it maps to the framework specs + +| Framework Spec | Implementation | +|----------------|---------------| +| Phase 2.2 WF-VM-01 | `src/workflows/performance-tracking.ts` | +| Phase 2.2 WF-VM-02 | `src/workflows/delegation-flow-analysis.ts` | +| Phase 2.2 WF-VM-03 | `src/workflows/decentralization-monitor.ts` | +| Phase 2.4 OODA executor | `src/ooda.ts` | +| Phase 2.4 Agent character | `agents/packages/agents/src/characters/validator-monitor.ts` | +| Phase 2.5 Workflow executions table | `src/store.ts` (SQLite) | +| Phase 3.2 Ledger MCP client | `src/ledger.ts` (direct LCD) | +| M014 scoring methodology | `src/workflows/performance-tracking.ts` | + +## Scoring methodology (M014) + +Composite score is **0-1000**, computed deterministically in `performance-tracking.ts`: + +| Component | Weight | Source | +|---|---|---| +| Uptime | 400 | `signed_blocks_window − missed_blocks_counter` from slashing signing info | +| Governance participation | 350 | Votes cast over recent finalized proposals (MVP keeps this at 0; see below) | +| Stability | 250 | 250 − (jailings × 100) − (commission changes in window × 40), floored at 0 | + +**PoA eligibility:** composite >= `config.validator.poaEligibilityScore` (default 800). + +## Design decisions + +1. **Deterministic numbers, narrative-only LLM calls.** All scoring, Nakamoto and Gini computation, health classification, and whale detection happen locally in plain TypeScript. Claude is only invoked to write the report. Keeps the agent cheap, reproducible, and auditable. + +2. **Token-delta as delegation source for the MVP.** WF-VM-02 snapshots each validator's `tokens` field every cycle and derives flows from the delta against the previous snapshot. A follow-up PR will plug in a real `MsgDelegate` / `MsgUndelegate` / `MsgRedelegate` tx-stream client. The code path swaps cleanly once real events are available. + +3. **Governance participation scoring is MVP-zero.** The operator→delegator bech32 conversion (required to fetch per-validator votes via `/proposals/{id}/votes/{voter}`) is deferred to a follow-up PR. All validators currently receive a 0 governance score, so relative composite scores still surface real differences in uptime and stability without punishing anyone asymmetrically. A separate PR will add the bech32 conversion and full vote fetching. + +4. **Nakamoto coefficient uses the 33.4% halt-threshold convention.** Defined as the smallest number of validators whose cumulative voting power strictly exceeds 33.4% of total bonded stake — matches the standard Cosmos-era definition. + +5. **Gini index uses the textbook formula** operating on ascending-sorted token amounts, normalized to uregen. 0 = perfect equality, 1 = perfect inequality. + +6. **Standalone over ElizaOS.** Matches AGENT-002 and AGENT-003. ElizaOS plugin API may change; a standalone process proves the workflow logic works independently of any runtime framework. + +## Governance layer + +This agent operates at **Layer 1 only**: + +- Read-only access to on-chain state +- Cannot delegate, undelegate, or redelegate +- Cannot submit proposals or votes +- Cannot execute transactions +- Informational output only + +Matches the framework's principle of starting with the lowest-risk, highest-value capability. Raising the automation layer (e.g., automated delegation rebalancing to under-concentrated validators) is a separate governance decision. diff --git a/agent-004-validator-monitor/package-lock.json b/agent-004-validator-monitor/package-lock.json new file mode 100644 index 0000000..8807065 --- /dev/null +++ b/agent-004-validator-monitor/package-lock.json @@ -0,0 +1,1475 @@ +{ + "name": "@regen/agent-004-validator-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@regen/agent-004-validator-monitor", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/agent-004-validator-monitor/package.json b/agent-004-validator-monitor/package.json new file mode 100644 index 0000000..fd10636 --- /dev/null +++ b/agent-004-validator-monitor/package.json @@ -0,0 +1,27 @@ +{ + "name": "@regen/agent-004-validator-monitor", + "version": "0.1.0", + "description": "Regen Network Validator Monitor Agent — Layer 1 read-only validator performance tracking, delegation flow analysis, and network decentralization monitoring", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node --import tsx src/index.ts", + "dev": "node --import tsx --watch src/index.ts", + "analyze": "node --import tsx src/index.ts --once", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/agent-004-validator-monitor/src/config.ts b/agent-004-validator-monitor/src/config.ts new file mode 100644 index 0000000..1092304 --- /dev/null +++ b/agent-004-validator-monitor/src/config.ts @@ -0,0 +1,62 @@ +export const config = { + // Regen LCD endpoint + lcdUrl: process.env.REGEN_LCD_URL || "https://regen.api.chandrastation.com", + + // Anthropic + anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", + model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", + + // Discord webhook (optional) + discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || "", + + // Polling — validator state moves slowly; 15 min is plenty and + // keeps the agent off the LCD's rate-limit radar. + pollIntervalMs: (parseInt(process.env.POLL_INTERVAL_SECONDS || "900", 10)) * 1000, + + // Validator monitor thresholds. These mirror the character + // thresholds in + // agents/packages/agents/src/characters/validator-monitor.ts so + // downstream tooling has a single source of truth. Keep them in + // sync if either file changes. + validator: { + /** Concentration % (of a single validator) that triggers CRITICAL */ + criticalConcentrationPct: 33, + /** Concentration % that triggers WARNING */ + warningConcentrationPct: 20, + /** Minimum composite score (0-1000) for M014 PoA eligibility */ + poaEligibilityScore: 800, + /** Uptime component weight (of 1000) */ + scoreWeightUptime: 400, + /** Governance participation component weight (of 1000) */ + scoreWeightGovernance: 350, + /** Stability component weight (of 1000) */ + scoreWeightStability: 250, + /** Trailing window for uptime scoring (days) — used for the narrative, not the math */ + uptimeTrailingDays: 30, + /** Per-commission-change stability penalty */ + stabilityPenaltyCommissionChange: 40, + /** Per-jailing stability penalty */ + stabilityPenaltyJailing: 100, + /** Whale movement threshold (REGEN, in uregen) — flags delegation deltas */ + whaleDelegationUregen: "100000000000", // 100,000 REGEN + /** Nakamoto floor below which WF-VM-03 warns */ + nakamotoWarningFloor: 8, + /** Nakamoto floor below which WF-VM-03 escalates */ + nakamotoCriticalFloor: 5, + /** Gini ceiling above which WF-VM-03 warns */ + giniWarningCeiling: 0.65, + }, + + // Agent identity + agentId: "AGENT-004", + agentName: "RegenValidatorMonitor", + governanceLayer: 1 as const, +} as const; + +export function validateConfig(): void { + if (!config.anthropicApiKey) { + throw new Error( + "ANTHROPIC_API_KEY is required. Copy .env.example to .env and set it." + ); + } +} diff --git a/agent-004-validator-monitor/src/index.ts b/agent-004-validator-monitor/src/index.ts new file mode 100644 index 0000000..02aef72 --- /dev/null +++ b/agent-004-validator-monitor/src/index.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env node +import { config, validateConfig } from "./config.js"; +import { LedgerClient } from "./ledger.js"; +import { executeOODA } from "./ooda.js"; +import { store } from "./store.js"; +import { createPerformanceTrackingWorkflow } from "./workflows/performance-tracking.js"; +import { createDelegationFlowAnalysisWorkflow } from "./workflows/delegation-flow-analysis.js"; +import { createDecentralizationMonitorWorkflow } from "./workflows/decentralization-monitor.js"; + +// ── Banner ──────────────────────────────────────────────────── + +function banner() { + console.log(` + ╔══════════════════════════════════════════════════════════════╗ + ║ REGEN VALIDATOR MONITOR (AGENT-004) ║ + ║ ║ + ║ Layer 1 — Fully Automated, Informational Only ║ + ║ Workflows: WF-VM-01, WF-VM-02, WF-VM-03 ║ + ║ ║ + ║ Regen Agentic Tokenomics Framework ║ + ╚══════════════════════════════════════════════════════════════╝ +`); +} + +// ── Main loop ───────────────────────────────────────────────── + +async function runCycle(ledger: LedgerClient): Promise { + const ts = new Date().toISOString(); + console.log(`\n[${ts}] ═══ Starting validator monitor cycle ═══\n`); + + // WF-VM-01: Performance tracking + alerts + const wf01 = createPerformanceTrackingWorkflow(ledger); + await executeOODA(wf01); + + // WF-VM-02: Delegation flow analysis + const wf02 = createDelegationFlowAnalysisWorkflow(ledger); + await executeOODA(wf02); + + // WF-VM-03: Decentralization monitoring + const wf03 = createDecentralizationMonitorWorkflow(ledger); + await executeOODA(wf03); + + const execCount = store.getExecutionCount(); + console.log( + `[${new Date().toISOString()}] ═══ Cycle complete (${execCount} total executions logged) ═══\n` + ); +} + +async function main() { + banner(); + validateConfig(); + + const runOnce = process.argv.includes("--once"); + const ledger = new LedgerClient(); + + console.log(`Configuration:`); + console.log(` LCD endpoint: ${config.lcdUrl}`); + console.log(` LLM model: ${config.model}`); + console.log(` Discord: ${config.discordWebhookUrl ? "configured" : "not configured"}`); + console.log(` Mode: ${runOnce ? "single run" : `polling every ${config.pollIntervalMs / 1000}s`}`); + console.log(); + + try { + const { blockHeight } = await ledger.checkConnection(); + console.log(`Connected to Regen Ledger at block ${blockHeight}\n`); + } catch (err) { + console.error( + `Failed to connect to Regen Ledger at ${config.lcdUrl}:`, + err + ); + process.exit(1); + } + + if (runOnce) { + await runCycle(ledger); + } else { + await runCycle(ledger); + + const interval = setInterval(() => { + runCycle(ledger).catch((err) => + console.error(`Cycle failed:`, err) + ); + }, config.pollIntervalMs); + + const shutdown = () => { + console.log("\nShutting down gracefully..."); + clearInterval(interval); + store.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + console.log("Agent running. Press Ctrl+C to stop.\n"); + } +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/agent-004-validator-monitor/src/ledger.ts b/agent-004-validator-monitor/src/ledger.ts new file mode 100644 index 0000000..8dce42a --- /dev/null +++ b/agent-004-validator-monitor/src/ledger.ts @@ -0,0 +1,151 @@ +import { config } from "./config.js"; +import type { + Validator, + SigningInfo, + SlashingParams, + StakingPool, + Proposal, + Vote, +} from "./types.js"; + +/** + * Regen Ledger LCD (REST) client — staking, slashing, gov endpoints. + * + * Talks directly to a Cosmos LCD endpoint — no MCP dependency. Matches + * the pattern used by AGENT-002 and AGENT-003. When Ledger MCP becomes + * available, this can be swapped behind the same interface. + */ +export class LedgerClient { + private baseUrl: string; + + constructor(baseUrl?: string) { + this.baseUrl = (baseUrl || config.lcdUrl).replace(/\/$/, ""); + } + + // ── Staking ────────────────────────────────────────────────── + + async getValidators(status = "BOND_STATUS_BONDED"): Promise { + const params = new URLSearchParams(); + params.set("status", status); + params.set("pagination.limit", "500"); + + const data = await this.get( + `/cosmos/staking/v1beta1/validators?${params.toString()}` + ); + return (data.validators || []) as Validator[]; + } + + async getValidator(operatorAddress: string): Promise { + try { + const data = await this.get( + `/cosmos/staking/v1beta1/validators/${operatorAddress}` + ); + return (data.validator || null) as Validator | null; + } catch { + return null; + } + } + + async getStakingPool(): Promise { + const data = await this.get(`/cosmos/staking/v1beta1/pool`); + return data.pool as StakingPool; + } + + // ── Slashing ───────────────────────────────────────────────── + + async getSigningInfos(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "500"); + + const data = await this.get( + `/cosmos/slashing/v1beta1/signing_infos?${params.toString()}` + ); + return (data.info || []) as SigningInfo[]; + } + + async getSlashingParams(): Promise { + try { + const data = await this.get(`/cosmos/slashing/v1beta1/params`); + return (data.params || null) as SlashingParams | null; + } catch { + return null; + } + } + + // ── Governance (for participation scoring) ─────────────────── + + async getRecentFinalizedProposals(limit = 20): Promise { + // Pull the most recent passed + rejected proposals. Passed = "3", + // Rejected = "4" in the Cosmos gov v1beta1 status enum. + const params = new URLSearchParams(); + params.set("pagination.limit", String(limit)); + params.set("pagination.reverse", "true"); + + const results: Proposal[] = []; + for (const status of ["3", "4"]) { + params.set("proposal_status", status); + try { + const data = await this.get( + `/cosmos/gov/v1beta1/proposals?${params.toString()}` + ); + const list = (data.proposals || []) as Proposal[]; + results.push(...list); + } catch { + // ignore; partial proposal history still produces a useful score + } + } + + // Newest first, deduped by id, capped at `limit` + const seen = new Set(); + const ordered = results + .slice() + .sort((a, b) => Number(b.id) - Number(a.id)) + .filter((p) => { + if (seen.has(p.id)) return false; + seen.add(p.id); + return true; + }) + .slice(0, limit); + return ordered; + } + + async getVoteForVoter( + proposalId: string, + voter: string + ): Promise { + try { + const data = await this.get( + `/cosmos/gov/v1beta1/proposals/${proposalId}/votes/${voter}` + ); + return (data.vote || null) as Vote | null; + } catch { + return null; + } + } + + // ── Connectivity check ────────────────────────────────────── + + async checkConnection(): Promise<{ blockHeight: string }> { + const data = await this.get(`/cosmos/base/tendermint/v1beta1/blocks/latest`); + const block = data.block as Record | undefined; + const header = block?.header as Record | undefined; + const height = (header?.height as string) || "unknown"; + return { blockHeight: height }; + } + + // ── HTTP ───────────────────────────────────────────────────── + + private async get(path: string): Promise> { + const url = `${this.baseUrl}${path}`; + const res = await fetch(url, { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + throw new Error(`LCD ${res.status}: ${res.statusText} — ${url}`); + } + + return (await res.json()) as Record; + } +} diff --git a/agent-004-validator-monitor/src/monitor.ts b/agent-004-validator-monitor/src/monitor.ts new file mode 100644 index 0000000..17b2758 --- /dev/null +++ b/agent-004-validator-monitor/src/monitor.ts @@ -0,0 +1,201 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { config } from "./config.js"; +import type { + ValidatorScorecard, + DelegationFlowSummary, + DecentralizationSnapshot, + PerformanceAlert, +} from "./types.js"; + +const client = new Anthropic({ apiKey: config.anthropicApiKey }); + +/** + * System prompt mirrors the AGENT-004 character definition at + * agents/packages/agents/src/characters/validator-monitor.ts. All + * thresholds are injected from config so the deterministic pipeline + * and the narrative layer cannot drift. + */ +const SYSTEM_PROMPT = `You are the Regen Validator Monitor Agent (AGENT-004). + +Your responsibilities: +1. Tracking validator uptime and block production performance +2. Monitoring governance voting participation +3. Analyzing delegation flows, whale movements, and concentration +4. Computing M014 performance scores +5. Assessing network decentralization and PoA transition readiness + +Core Principles: +- Prioritize network security and decentralization above all else +- Present performance data objectively +- Track trends over time, not just point-in-time snapshots +- Never recommend delegation choices; surface data for delegators to decide + +Scoring Methodology (M014): +- Uptime weight: ${config.validator.scoreWeightUptime} / 1000 +- Governance weight: ${config.validator.scoreWeightGovernance} / 1000 +- Stability weight: ${config.validator.scoreWeightStability} / 1000 +- PoA eligibility: composite >= ${config.validator.poaEligibilityScore} + +Alert Levels: +- NORMAL: Metrics within healthy bounds +- WARNING / HIGH: Degradation detected +- CRITICAL: Immediate risk to network health (e.g., single validator > ${config.validator.criticalConcentrationPct}% of stake) + +Output Format: +- Use markdown tables with units +- Include timeframes for all metrics +- Cite the deterministic numbers passed to you; do not invent values`; + +// ============================================================ +// WF-VM-01: Performance narrative +// ============================================================ + +export async function describePerformanceReport( + scorecards: ValidatorScorecard[], + alerts: PerformanceAlert[], + bondedUregen: string +): Promise { + const poaEligible = scorecards.filter((s) => s.poaEligible).length; + const top = scorecards.slice(0, 10); + + const prompt = `Generate a Validator Performance Report for the Regen Network active validator set. + +## Deterministic Pipeline Output +- Active set: ${scorecards.length} validators +- Bonded: ${bondedUregen} uregen +- PoA eligible (composite >= ${config.validator.poaEligibilityScore}): ${poaEligible}/${scorecards.length} + +## Top 10 Validators by Composite Score +${top + .map( + (s, i) => + `${i + 1}. ${s.moniker} (${s.operatorAddress.slice(0, 14)}…) — composite ${s.compositeScore} (uptime ${s.uptimeScore}, gov ${s.governanceScore}, stability ${s.stabilityScore}) — stake ${s.stakePct.toFixed(2)}%` + ) + .join("\n")} + +## Alerts (${alerts.length}) +${ + alerts.length === 0 + ? "- None" + : alerts.map((a) => `- ${a.severity}: ${a.moniker} — ${a.reason}`).join("\n") +} + +Generate a structured Markdown report with: +1. Header "Validator Performance Report" with active set size and PoA eligibility ratio +2. "Top 10 Validators" table with: rank, moniker, uptime ratio, gov participation, stability, composite +3. "Scoring Methodology (M014)" block using ONLY the weights passed in the prompt +4. "Alerts This Period" bullet list using ONLY the alerts passed in +5. "PoA Transition Readiness" section — ratio of validators meeting the threshold, no speculation about when PoA will activate + +Use ONLY the numbers provided. Do not recommend delegation choices.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1500, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-VM-02: Delegation flow narrative +// ============================================================ + +export async function describeDelegationFlows( + summary: DelegationFlowSummary +): Promise { + const prompt = `Generate a Delegation Flow Report for the Regen staking set. + +## Deterministic Pipeline Output +- Window: ${summary.windowLabel} +- Validators with any flow: ${summary.validatorsWithFlow} +- Total inflow: ${summary.totalInflowUregen} uregen +- Total outflow: ${summary.totalOutflowUregen} uregen +- Net flow (signed): ${summary.netFlowUregen} uregen +- Whale-sized flows (>= ${config.validator.whaleDelegationUregen} uregen): ${summary.whaleFlowCount} +- Top inflow validator: ${summary.topInflow ? `${summary.topInflow.moniker} (+${summary.topInflow.deltaAbsUregen} uregen)` : "(none)"} +- Top outflow validator: ${summary.topOutflow ? `${summary.topOutflow.moniker} (-${summary.topOutflow.deltaAbsUregen} uregen)` : "(none)"} +- Captured: ${summary.capturedAt} + +Generate a structured Markdown report with: +1. Header "Delegation Flow Report" with severity chosen from the whale count and net flow sign +2. "Flow Summary" table with inflow / outflow / net / whale count +3. "Notable Movements" section listing top inflow and top outflow (or a note if none) +4. "Assessment" section — one or two sentences noting whether the net flow direction warrants continued monitoring + +Use ONLY the numbers provided. Do not predict future delegation behavior.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-VM-03: Decentralization narrative +// ============================================================ + +export async function describeDecentralizationSnapshot( + snapshot: DecentralizationSnapshot, + previous: DecentralizationSnapshot | null +): Promise { + const prev = previous + ? ` +## Previous Snapshot (${previous.capturedAt}) +- Validator count: ${previous.validatorCount} +- Nakamoto: ${previous.nakamotoCoefficient} +- Gini: ${previous.giniIndex.toFixed(3)} +- Top-10 share: ${previous.top10SharePct.toFixed(2)}% +- Health: ${previous.health} +` + : "## Previous Snapshot\nNone — this is the first decentralization snapshot."; + + const prompt = `Generate a Network Decentralization Report for Regen Network. + +## Deterministic Pipeline Output +- Active validator count: ${snapshot.validatorCount} +- Bonded: ${snapshot.bondedUregen} uregen +- Nakamoto coefficient: ${snapshot.nakamotoCoefficient} +- Gini index: ${snapshot.giniIndex.toFixed(3)} +- Top-10 share: ${snapshot.top10SharePct.toFixed(2)}% +- Top-20 share: ${snapshot.top20SharePct.toFixed(2)}% +- Largest validator: ${snapshot.largestShareValidator} (${snapshot.largestSharePct.toFixed(2)}%) +- Health: ${snapshot.health} +- Captured: ${snapshot.capturedAt} + +${prev} + +Generate a structured Markdown report with: +1. Header "Network Decentralization Report" with health badge +2. "Metrics" table with current snapshot values +3. "Trend" section comparing to the previous snapshot (or noting this is the first) +4. "Assessment" — one paragraph interpreting the Nakamoto coefficient and Gini. If health is WARNING or CRITICAL, explain what triggered it using the thresholds in config (nakamoto < ${config.validator.nakamotoWarningFloor}, gini > ${config.validator.giniWarningCeiling}, single validator > ${config.validator.criticalConcentrationPct}%) + +Use ONLY the numbers provided. Do not speculate about future decentralization.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1200, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// Helpers +// ============================================================ + +function extractText(response: Anthropic.Message): string { + return response.content + .filter((b): b is Anthropic.TextBlock => b.type === "text") + .map((b) => b.text) + .join("\n"); +} diff --git a/agent-004-validator-monitor/src/ooda.ts b/agent-004-validator-monitor/src/ooda.ts new file mode 100644 index 0000000..a05e332 --- /dev/null +++ b/agent-004-validator-monitor/src/ooda.ts @@ -0,0 +1,83 @@ +import { config } from "./config.js"; +import { store } from "./store.js"; +import type { OODAExecution } from "./types.js"; + +/** + * Generic OODA loop executor. + * + * Same shape as agent-002 and agent-003. Kept locally so this agent + * can run as a standalone Node process. If the agents converge on a + * common runtime later, this is a candidate to lift into a shared + * package. + */ +export interface OODAWorkflow { + id: string; + name: string; + observe: () => Promise; + orient: (observations: TObserve) => Promise; + decide: (orientation: TOrient) => Promise; + act: (decision: TDecide) => Promise; +} + +export async function executeOODA( + workflow: OODAWorkflow +): Promise> { + const executionId = crypto.randomUUID(); + const execution: OODAExecution = { + executionId, + workflowId: workflow.id, + status: "running", + observations: null as unknown as TObserve, + orientation: null, + decision: null, + actions: null, + startedAt: new Date(), + completedAt: null, + error: null, + }; + + const log = (phase: string, msg: string) => + console.log( + `[${new Date().toISOString()}] [${workflow.id}] [${phase}] ${msg}` + ); + + try { + log("OBSERVE", "Gathering data..."); + execution.observations = await workflow.observe(); + + log("ORIENT", "Analyzing context..."); + execution.orientation = await workflow.orient(execution.observations); + + log("DECIDE", "Making decision..."); + execution.decision = await workflow.decide(execution.orientation); + + log("ACT", "Executing actions..."); + execution.actions = await workflow.act(execution.decision); + + execution.status = "completed"; + log("DONE", "Workflow completed successfully."); + } catch (err) { + execution.status = "failed"; + execution.error = err instanceof Error ? err.message : String(err); + log("ERROR", execution.error); + } + + execution.completedAt = new Date(); + + store.logExecution({ + executionId: execution.executionId, + workflowId: execution.workflowId, + agentId: config.agentId, + status: execution.status, + startedAt: execution.startedAt.toISOString(), + completedAt: execution.completedAt.toISOString(), + result: JSON.stringify({ + orientation: execution.orientation, + decision: execution.decision, + actions: execution.actions, + error: execution.error, + }), + }); + + return execution; +} diff --git a/agent-004-validator-monitor/src/output.ts b/agent-004-validator-monitor/src/output.ts new file mode 100644 index 0000000..64f3ee9 --- /dev/null +++ b/agent-004-validator-monitor/src/output.ts @@ -0,0 +1,58 @@ +import { config } from "./config.js"; +import type { OutputMessage } from "./types.js"; + +/** + * Output dispatcher. + * + * Sends agent outputs to configured channels: + * - Console (always) + * - Discord webhook (if configured) + * + * Mirrors the AGENT-002 and AGENT-003 dispatcher shape. + */ +export async function output(msg: OutputMessage): Promise { + logToConsole(msg); + + if (config.discordWebhookUrl) { + await postToDiscord(msg).catch((err) => + console.error(` Discord post failed: ${err}`) + ); + } +} + +function logToConsole(msg: OutputMessage): void { + const prefix = + msg.alertLevel === "CRITICAL" + ? "!!! CRITICAL" + : msg.alertLevel === "HIGH" + ? "!! HIGH" + : "--"; + + console.log(`\n${"=".repeat(72)}`); + console.log( + `${prefix} [${msg.workflow}] ${msg.subjectId}: ${msg.title}` + ); + console.log(`${"=".repeat(72)}`); + console.log(msg.content); + console.log(`${"=".repeat(72)}\n`); +} + +async function postToDiscord(msg: OutputMessage): Promise { + const maxLen = 1900; + let content = `**[${msg.workflow}]** ${msg.subjectId}: **${msg.title}**\n\n`; + const remaining = maxLen - content.length; + content += + msg.content.length > remaining + ? msg.content.slice(0, remaining - 20) + "\n\n*...truncated*" + : msg.content; + + await fetch(config.discordWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: config.agentName, + content, + }), + signal: AbortSignal.timeout(10_000), + }); +} diff --git a/agent-004-validator-monitor/src/store.ts b/agent-004-validator-monitor/src/store.ts new file mode 100644 index 0000000..5608562 --- /dev/null +++ b/agent-004-validator-monitor/src/store.ts @@ -0,0 +1,300 @@ +import Database from "better-sqlite3"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DB_PATH = path.join(__dirname, "..", "agent-004.db"); + +/** + * Local SQLite store for AGENT-004 state. + * + * Tracks per-validator token snapshots (used as the delta source for + * WF-VM-02 until a real tx-stream lands), scorecards, decentralization + * snapshots, and workflow execution history. + */ +class Store { + private db: Database.Database; + + constructor() { + this.db = new Database(DB_PATH); + this.db.pragma("journal_mode = WAL"); + this.migrate(); + } + + private migrate() { + this.db.exec(` + CREATE TABLE IF NOT EXISTS workflow_executions ( + execution_id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL, + agent_id TEXT NOT NULL, + status TEXT NOT NULL, + started_at TEXT NOT NULL, + completed_at TEXT, + result TEXT + ); + + -- Snapshot of a validator's staked tokens at a point in time. + -- WF-VM-02 reads the most recent previous row per operator to + -- derive the per-cycle delegation delta. + CREATE TABLE IF NOT EXISTS validator_token_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL, + moniker TEXT NOT NULL, + tokens TEXT NOT NULL, + commission_rate TEXT NOT NULL, + jailed INTEGER NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Snapshot of the commission rate timeline per validator. WF-VM-01 + -- uses this to count commission changes in the stability score. + CREATE TABLE IF NOT EXISTS commission_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL, + commission_rate TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Cached scorecards keyed by operator + captured_at so the + -- narrative layer can look up the previous scorecard for a + -- validator when reporting a change. + CREATE TABLE IF NOT EXISTS scorecards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL, + composite_score INTEGER NOT NULL, + scorecard TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Decentralization snapshots (WF-VM-03) + CREATE TABLE IF NOT EXISTS decentralization_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nakamoto INTEGER NOT NULL, + gini REAL NOT NULL, + top10_pct REAL NOT NULL, + health TEXT NOT NULL, + snapshot TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_tokens_operator ON validator_token_snapshots(operator_address); + CREATE INDEX IF NOT EXISTS idx_commission_operator ON commission_history(operator_address); + CREATE INDEX IF NOT EXISTS idx_scorecards_operator ON scorecards(operator_address); + `); + } + + // ── Token snapshots (for WF-VM-02 delta source) ──────────── + + recordTokenSnapshot(row: { + operatorAddress: string; + moniker: string; + tokens: string; + commissionRate: string; + jailed: boolean; + }): void { + this.db + .prepare( + `INSERT INTO validator_token_snapshots + (operator_address, moniker, tokens, commission_rate, jailed, captured_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + row.operatorAddress, + row.moniker, + row.tokens, + row.commissionRate, + row.jailed ? 1 : 0, + new Date().toISOString() + ); + } + + getPreviousTokenSnapshot( + operatorAddress: string, + excludeLatest: boolean + ): { tokens: string; captured_at: string } | null { + // When excludeLatest is true, skip the most recent row (the one we + // just wrote this cycle) and return the one before it. + const offset = excludeLatest ? 1 : 0; + const row = this.db + .prepare( + `SELECT tokens, captured_at FROM validator_token_snapshots + WHERE operator_address = ? ORDER BY id DESC LIMIT 1 OFFSET ?` + ) + .get(operatorAddress, offset) as + | { tokens: string; captured_at: string } + | undefined; + return row || null; + } + + // ── Commission history ───────────────────────────────────── + + recordCommissionIfChanged( + operatorAddress: string, + commissionRate: string + ): boolean { + const latest = this.db + .prepare( + `SELECT commission_rate FROM commission_history + WHERE operator_address = ? ORDER BY id DESC LIMIT 1` + ) + .get(operatorAddress) as { commission_rate: string } | undefined; + if (latest && latest.commission_rate === commissionRate) return false; + + this.db + .prepare( + `INSERT INTO commission_history + (operator_address, commission_rate, captured_at) + VALUES (?, ?, ?)` + ) + .run(operatorAddress, commissionRate, new Date().toISOString()); + return true; + } + + countCommissionChangesSince( + operatorAddress: string, + sinceIso: string + ): number { + // Every row after the first recorded rate represents a change. + // Subtract 1 to discount the baseline row (if any) that predates + // the window. + const row = this.db + .prepare( + `SELECT COUNT(*) AS cnt FROM commission_history + WHERE operator_address = ? AND captured_at >= ?` + ) + .get(operatorAddress, sinceIso) as { cnt: number }; + return Math.max(0, row.cnt - 1); + } + + // ── Scorecards ───────────────────────────────────────────── + + saveScorecard(row: { + operatorAddress: string; + compositeScore: number; + scorecard: string; + }): void { + this.db + .prepare( + `INSERT INTO scorecards + (operator_address, composite_score, scorecard, captured_at) + VALUES (?, ?, ?, ?)` + ) + .run( + row.operatorAddress, + row.compositeScore, + row.scorecard, + new Date().toISOString() + ); + } + + getLatestScorecard( + operatorAddress: string + ): { compositeScore: number; scorecard: string; capturedAt: string } | null { + const row = this.db + .prepare( + `SELECT composite_score, scorecard, captured_at FROM scorecards + WHERE operator_address = ? ORDER BY id DESC LIMIT 1` + ) + .get(operatorAddress) as + | { composite_score: number; scorecard: string; captured_at: string } + | undefined; + if (!row) return null; + return { + compositeScore: row.composite_score, + scorecard: row.scorecard, + capturedAt: row.captured_at, + }; + } + + // ── Decentralization snapshots ───────────────────────────── + + saveDecentralizationSnapshot(row: { + nakamoto: number; + gini: number; + top10Pct: number; + health: string; + snapshot: string; + }): void { + this.db + .prepare( + `INSERT INTO decentralization_snapshots + (nakamoto, gini, top10_pct, health, snapshot, captured_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + row.nakamoto, + row.gini, + row.top10Pct, + row.health, + row.snapshot, + new Date().toISOString() + ); + } + + getLatestDecentralizationSnapshot(): { + nakamoto: number; + gini: number; + top10_pct: number; + health: string; + snapshot: string; + captured_at: string; + } | null { + const row = this.db + .prepare( + `SELECT nakamoto, gini, top10_pct, health, snapshot, captured_at + FROM decentralization_snapshots ORDER BY id DESC LIMIT 1 OFFSET 1` + ) + .get() as + | { + nakamoto: number; + gini: number; + top10_pct: number; + health: string; + snapshot: string; + captured_at: string; + } + | undefined; + return row || null; + } + + // ── Workflow executions ──────────────────────────────────── + + logExecution(exec: { + executionId: string; + workflowId: string; + agentId: string; + status: string; + startedAt: string; + completedAt: string; + result: string; + }): void { + this.db + .prepare( + `INSERT INTO workflow_executions + (execution_id, workflow_id, agent_id, status, started_at, completed_at, result) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run( + exec.executionId, + exec.workflowId, + exec.agentId, + exec.status, + exec.startedAt, + exec.completedAt, + exec.result + ); + } + + getExecutionCount(): number { + const row = this.db + .prepare("SELECT COUNT(*) as cnt FROM workflow_executions") + .get() as { cnt: number }; + return row.cnt; + } + + close(): void { + this.db.close(); + } +} + +export const store = new Store(); diff --git a/agent-004-validator-monitor/src/types.ts b/agent-004-validator-monitor/src/types.ts new file mode 100644 index 0000000..0d0d4d0 --- /dev/null +++ b/agent-004-validator-monitor/src/types.ts @@ -0,0 +1,170 @@ +// ============================================================ +// Cosmos SDK staking + slashing + gov types +// ============================================================ + +export interface Validator { + operator_address: string; + consensus_pubkey: { "@type": string; key: string }; + jailed: boolean; + status: string; + tokens: string; + delegator_shares: string; + description: { + moniker: string; + identity: string; + website: string; + security_contact: string; + details: string; + }; + unbonding_height: string; + unbonding_time: string; + commission: { + commission_rates: { + rate: string; + max_rate: string; + max_change_rate: string; + }; + update_time: string; + }; + min_self_delegation: string; +} + +export interface SigningInfo { + address: string; + start_height: string; + index_offset: string; + jailed_until: string; + tombstoned: boolean; + missed_blocks_counter: string; +} + +export interface SlashingParams { + signed_blocks_window: string; + min_signed_per_window: string; + downtime_jail_duration: string; + slash_fraction_double_sign: string; + slash_fraction_downtime: string; +} + +export interface StakingPool { + bonded_tokens: string; + not_bonded_tokens: string; +} + +export interface Proposal { + id: string; + status: string; + voting_start_time: string; + voting_end_time: string; + content: { "@type": string; title: string; description: string }; +} + +export interface Vote { + proposal_id: string; + voter: string; + option: string; + options: { option: string; weight: string }[]; +} + +// ============================================================ +// OODA loop types +// ============================================================ + +export interface OODAExecution { + executionId: string; + workflowId: string; + status: "running" | "completed" | "failed" | "escalated"; + observations: TObserve; + orientation: TOrient | null; + decision: TDecide | null; + actions: TAct | null; + startedAt: Date; + completedAt: Date | null; + error: string | null; +} + +// ============================================================ +// Workflow-specific types +// ============================================================ + +export type AlertLevel = "NORMAL" | "HIGH" | "CRITICAL"; + +/** Validator performance scorecard (WF-VM-01) */ +export interface ValidatorScorecard { + operatorAddress: string; + moniker: string; + jailed: boolean; + tokens: string; + stakePct: number; + commissionRate: number; + uptimeScore: number; // 0..scoreWeightUptime + uptimeRatio: number; // 0..1 + missedBlocks: number; + signedBlocksWindow: number; + governanceScore: number; // 0..scoreWeightGovernance + governanceParticipation: number; // 0..1 + proposalsConsidered: number; + votesCast: number; + stabilityScore: number; // 0..scoreWeightStability + compositeScore: number; // 0..1000 + poaEligible: boolean; +} + +export interface PerformanceAlert { + operatorAddress: string; + moniker: string; + severity: AlertLevel; + reason: string; +} + +/** Delegation flow analysis (WF-VM-02) */ +export interface DelegationFlow { + operatorAddress: string; + moniker: string; + previousTokens: string; + currentTokens: string; + deltaUregen: string; // signed + deltaAbsUregen: string; + isWhale: boolean; + flowDirection: "INFLOW" | "OUTFLOW" | "FLAT"; + capturedAt: string; +} + +export interface DelegationFlowSummary { + windowLabel: string; + totalInflowUregen: string; + totalOutflowUregen: string; + netFlowUregen: string; // signed + validatorsWithFlow: number; + whaleFlowCount: number; + topInflow: DelegationFlow | null; + topOutflow: DelegationFlow | null; + capturedAt: string; +} + +/** Network decentralization (WF-VM-03) */ +export interface DecentralizationSnapshot { + validatorCount: number; + bondedUregen: string; + nakamotoCoefficient: number; + giniIndex: number; + top10SharePct: number; + top20SharePct: number; + largestShareValidator: string; + largestSharePct: number; + health: "HEALTHY" | "WARNING" | "CRITICAL"; + capturedAt: string; +} + +// ============================================================ +// Output types +// ============================================================ + +export interface OutputMessage { + workflow: string; + subjectId: string; + title: string; + content: string; + alertLevel: AlertLevel; + timestamp: Date; +} diff --git a/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts new file mode 100644 index 0000000..511a060 --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts @@ -0,0 +1,223 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeDecentralizationSnapshot } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Validator, + DecentralizationSnapshot, + AlertLevel, +} from "../types.js"; + +/** + * WF-VM-03: Network Decentralization Monitoring + * + * Trigger: periodic (every cycle) OR validator set change + * Layer: 1 (alerts) / Layer 3 (if action needed) + * + * OODA: + * Observe — Fetch active validator set. + * Orient — Compute Nakamoto coefficient, Gini index, top-N shares. + * Classify health tier. + * Decide — Generate narrative via Claude. + * Act — Persist snapshot, output, alert on threshold breach. + */ + +interface Observations { + validators: Validator[]; +} + +interface Orientation { + snapshot: DecentralizationSnapshot; + previous: DecentralizationSnapshot | null; +} + +interface Decision { + snapshot: DecentralizationSnapshot; + previous: DecentralizationSnapshot | null; + report: string; + alertLevel: AlertLevel; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +/** Smallest number of validators whose cumulative share of the total + * bonded stake strictly exceeds 33.4%. Consensus breaks at 2/3+1, so + * the canonical Cosmos-era Nakamoto coefficient uses 33% as the + * halt-threshold proxy. */ +function nakamotoCoefficient(sortedDescTokens: bigint[], total: bigint): number { + if (total === 0n || sortedDescTokens.length === 0) return 0; + const threshold = (total * 334n) / 1000n; // 33.4% + let cumulative = 0n; + for (let i = 0; i < sortedDescTokens.length; i++) { + cumulative += sortedDescTokens[i]!; + if (cumulative > threshold) return i + 1; + } + return sortedDescTokens.length; +} + +/** Gini index of a distribution. 0 = perfect equality, 1 = perfect + * inequality. Operates on ascending-sorted numbers and uses the + * textbook formula. */ +function giniIndex(sortedAscTokens: number[]): number { + const n = sortedAscTokens.length; + if (n === 0) return 0; + let sum = 0; + let cumulative = 0; + for (let i = 0; i < n; i++) { + cumulative += sortedAscTokens[i]!; + sum += (2 * (i + 1) - n - 1) * sortedAscTokens[i]!; + } + if (cumulative === 0) return 0; + return Math.abs(sum / (n * cumulative)); +} + +function topNSharePct( + sortedDescTokens: bigint[], + total: bigint, + n: number +): number { + if (total === 0n) return 0; + let acc = 0n; + for (let i = 0; i < Math.min(n, sortedDescTokens.length); i++) { + acc += sortedDescTokens[i]!; + } + return Number((acc * 10000n) / total) / 100; +} + +function classifyHealth( + nakamoto: number, + gini: number, + largestSharePct: number +): DecentralizationSnapshot["health"] { + if ( + nakamoto <= config.validator.nakamotoCriticalFloor || + largestSharePct >= config.validator.criticalConcentrationPct + ) { + return "CRITICAL"; + } + if ( + nakamoto <= config.validator.nakamotoWarningFloor || + gini >= config.validator.giniWarningCeiling || + largestSharePct >= config.validator.warningConcentrationPct + ) { + return "WARNING"; + } + return "HEALTHY"; +} + +export function createDecentralizationMonitorWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-VM-03", + name: "Network Decentralization Monitoring", + + async observe(): Promise { + const validators = await ledger.getValidators(); + return { validators }; + }, + + async orient(obs: Observations): Promise { + const capturedAt = new Date().toISOString(); + const active = obs.validators.filter((v) => !v.jailed); + + const tokensBig = active.map((v) => BigInt(v.tokens || "0")); + const total = tokensBig.reduce((acc, t) => acc + t, 0n); + const sortedDesc = [...tokensBig].sort((a, b) => (a > b ? -1 : a < b ? 1 : 0)); + + const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0") / 1_000_000n)); + const sortedAscNum = [...tokensNum].sort((a, b) => a - b); + + const nakamoto = nakamotoCoefficient(sortedDesc, total); + const gini = giniIndex(sortedAscNum); + const top10 = topNSharePct(sortedDesc, total, 10); + const top20 = topNSharePct(sortedDesc, total, 20); + + let largest = 0n; + let largestMoniker = "(unknown)"; + let largestPct = 0; + for (const v of active) { + const t = BigInt(v.tokens || "0"); + if (t > largest) { + largest = t; + largestMoniker = v.description.moniker; + largestPct = total > 0n ? Number((t * 10000n) / total) / 100 : 0; + } + } + + const health = classifyHealth(nakamoto, gini, largestPct); + + const snapshot: DecentralizationSnapshot = { + validatorCount: active.length, + bondedUregen: total.toString(), + nakamotoCoefficient: nakamoto, + giniIndex: gini, + top10SharePct: top10, + top20SharePct: top20, + largestShareValidator: largestMoniker, + largestSharePct: largestPct, + health, + capturedAt, + }; + + // Read the PREVIOUS snapshot BEFORE this cycle's save so the + // narrative layer has a real trend reference. + const prevRow = store.getLatestDecentralizationSnapshot(); + let previous: DecentralizationSnapshot | null = null; + if (prevRow) { + try { + previous = JSON.parse(prevRow.snapshot) as DecentralizationSnapshot; + } catch { + previous = null; + } + } + + return { snapshot, previous }; + }, + + async decide(orientation: Orientation): Promise { + const { snapshot, previous } = orientation; + const report = await describeDecentralizationSnapshot(snapshot, previous); + + const alertLevel: AlertLevel = + snapshot.health === "CRITICAL" + ? "CRITICAL" + : snapshot.health === "WARNING" + ? "HIGH" + : "NORMAL"; + + return { snapshot, previous, report, alertLevel }; + }, + + async act(decision: Decision): Promise { + const { snapshot, report, alertLevel } = decision; + + store.saveDecentralizationSnapshot({ + nakamoto: snapshot.nakamotoCoefficient, + gini: snapshot.giniIndex, + top10Pct: snapshot.top10SharePct, + health: snapshot.health, + snapshot: JSON.stringify(snapshot), + }); + + await output({ + workflow: "WF-VM-03", + subjectId: "network", + title: `${snapshot.health} — nakamoto ${snapshot.nakamotoCoefficient}, gini ${snapshot.giniIndex.toFixed(3)}`, + content: report, + alertLevel, + timestamp: new Date(), + }); + + return { + saved: 1, + alertsSent: alertLevel !== "NORMAL" ? 1 : 0, + }; + }, + }; +} diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts new file mode 100644 index 0000000..4eef1eb --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts @@ -0,0 +1,182 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeDelegationFlows } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Validator, + DelegationFlow, + DelegationFlowSummary, + AlertLevel, +} from "../types.js"; + +/** + * WF-VM-02: Delegation Flow Analysis + * + * Trigger: MsgDelegate / MsgUndelegate / MsgRedelegate (proxied by + * per-cycle `validator.tokens` deltas until a real tx-stream lands) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Snapshot current `tokens` per validator; pull the prior + * snapshot from SQLite and compute per-validator deltas. + * Orient — Aggregate inflow / outflow / net. Tag whale-sized + * movements. Pick top inflow and outflow. + * Decide — Generate flow summary via Claude. + * Act — Persist, output, alert on whale activity. + */ + +interface Observations { + validators: Validator[]; + previousTokensByOperator: Map; +} + +interface Orientation { + flows: DelegationFlow[]; + summary: DelegationFlowSummary; +} + +interface Decision { + summary: DelegationFlowSummary; + report: string; + alertLevel: AlertLevel; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +function absBig(x: bigint): bigint { + return x < 0n ? -x : x; +} + +export function createDelegationFlowAnalysisWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-VM-02", + name: "Delegation Flow Analysis", + + async observe(): Promise { + const validators = await ledger.getValidators(); + + const previousTokensByOperator = new Map(); + for (const v of validators) { + const prev = store.getPreviousTokenSnapshot(v.operator_address, false); + if (prev) previousTokensByOperator.set(v.operator_address, prev.tokens); + } + + // Write the new snapshots AFTER reading previous — we want the + // delta to be "previous cycle vs this cycle". + for (const v of validators) { + store.recordTokenSnapshot({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + tokens: v.tokens, + commissionRate: v.commission.commission_rates.rate, + jailed: v.jailed, + }); + } + + return { validators, previousTokensByOperator }; + }, + + async orient(obs: Observations): Promise { + const capturedAt = new Date().toISOString(); + const flows: DelegationFlow[] = []; + const whaleThreshold = BigInt(config.validator.whaleDelegationUregen); + + for (const v of obs.validators) { + const previousStr = obs.previousTokensByOperator.get(v.operator_address); + if (!previousStr) continue; // First time we see this validator; nothing to diff. + + const previous = BigInt(previousStr); + const current = BigInt(v.tokens || "0"); + const delta = current - previous; + const deltaAbs = absBig(delta); + if (delta === 0n) continue; + + flows.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + previousTokens: previousStr, + currentTokens: v.tokens, + deltaUregen: delta.toString(), + deltaAbsUregen: deltaAbs.toString(), + isWhale: deltaAbs >= whaleThreshold, + flowDirection: delta > 0n ? "INFLOW" : delta < 0n ? "OUTFLOW" : "FLAT", + capturedAt, + }); + } + + let totalInflow = 0n; + let totalOutflow = 0n; + let whaleFlowCount = 0; + let topInflow: DelegationFlow | null = null; + let topOutflow: DelegationFlow | null = null; + + for (const f of flows) { + const delta = BigInt(f.deltaUregen); + if (delta > 0n) { + totalInflow += delta; + if (!topInflow || delta > BigInt(topInflow.deltaUregen)) { + topInflow = f; + } + } else if (delta < 0n) { + totalOutflow += -delta; + if (!topOutflow || -delta > BigInt(topOutflow.deltaAbsUregen)) { + topOutflow = f; + } + } + if (f.isWhale) whaleFlowCount++; + } + + const summary: DelegationFlowSummary = { + windowLabel: "since previous cycle", + totalInflowUregen: totalInflow.toString(), + totalOutflowUregen: totalOutflow.toString(), + netFlowUregen: (totalInflow - totalOutflow).toString(), + validatorsWithFlow: flows.length, + whaleFlowCount, + topInflow, + topOutflow, + capturedAt, + }; + + return { flows, summary }; + }, + + async decide(orientation: Orientation): Promise { + const report = await describeDelegationFlows(orientation.summary); + + const alertLevel: AlertLevel = + orientation.summary.whaleFlowCount > 0 ? "HIGH" : "NORMAL"; + + return { + summary: orientation.summary, + report, + alertLevel, + }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + await output({ + workflow: "WF-VM-02", + subjectId: "delegation-flows", + title: `Net ${decision.summary.netFlowUregen} uregen (${decision.summary.whaleFlowCount} whale)`, + content: decision.report, + alertLevel: decision.alertLevel, + timestamp: new Date(), + }); + saved++; + if (decision.alertLevel !== "NORMAL") alertsSent++; + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-004-validator-monitor/src/workflows/performance-tracking.ts b/agent-004-validator-monitor/src/workflows/performance-tracking.ts new file mode 100644 index 0000000..192cb84 --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/performance-tracking.ts @@ -0,0 +1,305 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describePerformanceReport } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Validator, + SigningInfo, + SlashingParams, + StakingPool, + Proposal, + ValidatorScorecard, + PerformanceAlert, +} from "../types.js"; + +/** + * WF-VM-01: Validator Performance Tracking + * + * Trigger: periodic OR slash event + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch validator set, signing info, slashing params, + * staking pool, and recent finalized proposals (for + * governance participation scoring). + * Orient — For each validator: compute uptime ratio, governance + * participation ratio, stability score. Combine into a + * composite 0-1000. + * Decide — Flag performance alerts (uptime drop, jailed, high + * concentration). Persist scorecards. + * Act — Generate narrative report via Claude, output. + * + * Determinism: all scoring is local TypeScript. Claude only writes + * the narrative report. + */ + +interface Observations { + validators: Validator[]; + signingByConsAddrLike: Map; + slashingParams: SlashingParams | null; + pool: StakingPool; + finalizedProposals: Proposal[]; + votesCastByOperator: Map; +} + +interface Orientation { + scorecards: ValidatorScorecard[]; + alerts: PerformanceAlert[]; + bondedUregen: string; +} + +interface Decision { + report: string; + alertsToPublish: PerformanceAlert[]; + scorecards: ValidatorScorecard[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +function operatorToAccountBech32(_operatorAddress: string): string { + // Proper conversion from operator (regenvaloper1…) to delegator + // bech32 (regen1…) requires pulling out the underlying bytes and + // re-encoding with the delegator HRP. The MVP falls back to matching + // by moniker / position inside getVoteForVoter, so we return an + // empty string here and let the governance fetch short-circuit. + return ""; +} + +export function createPerformanceTrackingWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-VM-01", + name: "Validator Performance Tracking", + + async observe(): Promise { + const [validators, signingInfos, slashingParams, pool, finalizedProposals] = + await Promise.all([ + ledger.getValidators(), + ledger.getSigningInfos(), + ledger.getSlashingParams(), + ledger.getStakingPool(), + ledger.getRecentFinalizedProposals(20), + ]); + + // Index signing info by the consensus address field so we can + // match validators by their consensus-derived key. Signing info + // uses the valcons bech32 form; the validator object exposes + // `consensus_pubkey.key`. The MVP joins on the raw `address` + // field and falls back to `missed_blocks_counter = 0` when we + // can't resolve a match. + const signingByConsAddrLike = new Map(); + for (const info of signingInfos) { + signingByConsAddrLike.set(info.address, info); + } + + // Votes cast per operator — computed lazily here so the orient + // phase can compute the ratio without hitting the LCD again. + // Future work: convert the operator's valoper bech32 to the + // delegator bech32 and query /proposals/{id}/votes/{voter}. + const votesCastByOperator = new Map(); + + return { + validators, + signingByConsAddrLike, + slashingParams, + pool, + finalizedProposals, + votesCastByOperator, + }; + }, + + async orient(obs: Observations): Promise { + const { validators, signingByConsAddrLike, slashingParams, pool } = obs; + + const signedBlocksWindow = slashingParams + ? Number(slashingParams.signed_blocks_window) + : 10_000; + + const bonded = BigInt(pool.bonded_tokens || "0"); + const scorecards: ValidatorScorecard[] = []; + const alerts: PerformanceAlert[] = []; + + // Record a commission baseline for every validator we see this + // cycle; the commission history drives the stability penalty. + for (const v of validators) { + store.recordCommissionIfChanged( + v.operator_address, + v.commission.commission_rates.rate + ); + } + + for (const v of validators) { + const tokens = BigInt(v.tokens || "0"); + const stakePct = + bonded > 0n ? Number((tokens * 10000n) / bonded) / 100 : 0; + + // ── Uptime component ───────────────────────────────── + // When we can't find signing info, assume 100% (signed every + // window) rather than penalizing — better to under-count real + // issues than to smear a healthy validator. + const signing = signingByConsAddrLike.get( + v.consensus_pubkey?.key || "" + ); + const missedBlocks = signing + ? Number(signing.missed_blocks_counter || "0") + : 0; + const signedBlocks = Math.max(1, signedBlocksWindow - missedBlocks); + const uptimeRatio = Math.max( + 0, + Math.min(1, signedBlocks / signedBlocksWindow) + ); + const uptimeScore = Math.round( + uptimeRatio * config.validator.scoreWeightUptime + ); + + // ── Governance component ───────────────────────────── + // MVP: count finalized proposals as the denominator and give + // every validator credit for 0 votes (since the operator→account + // conversion is future work). This intentionally keeps the + // governance score at 0 across the set so no validator is + // punished relative to another. A follow-up PR will plug in + // real vote records per-validator. + void operatorToAccountBech32(v.operator_address); + const proposalsConsidered = obs.finalizedProposals.length; + const votesCast = obs.votesCastByOperator.get(v.operator_address) ?? 0; + const governanceParticipation = + proposalsConsidered > 0 ? votesCast / proposalsConsidered : 0; + const governanceScore = Math.round( + governanceParticipation * config.validator.scoreWeightGovernance + ); + + // ── Stability component ────────────────────────────── + // Start at full stability weight; subtract penalties for + // jailing and commission changes in the trailing window. + // Explicit `number` type so we can clamp to 0 below — `as + // const` in config.ts otherwise infers a literal type. + let stabilityScore: number = config.validator.scoreWeightStability; + if (v.jailed) stabilityScore -= config.validator.stabilityPenaltyJailing; + const sinceIso = new Date( + Date.now() - config.validator.uptimeTrailingDays * 86_400_000 + ).toISOString(); + const commissionChanges = store.countCommissionChangesSince( + v.operator_address, + sinceIso + ); + stabilityScore -= + commissionChanges * config.validator.stabilityPenaltyCommissionChange; + if (stabilityScore < 0) stabilityScore = 0; + + const compositeScore = + uptimeScore + governanceScore + stabilityScore; + const poaEligible = + compositeScore >= config.validator.poaEligibilityScore; + + const card: ValidatorScorecard = { + operatorAddress: v.operator_address, + moniker: v.description.moniker, + jailed: v.jailed, + tokens: v.tokens, + stakePct, + commissionRate: Number(v.commission.commission_rates.rate), + uptimeScore, + uptimeRatio, + missedBlocks, + signedBlocksWindow, + governanceScore, + governanceParticipation, + proposalsConsidered, + votesCast, + stabilityScore, + compositeScore, + poaEligible, + }; + scorecards.push(card); + + if (v.jailed) { + alerts.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + severity: "CRITICAL", + reason: "Validator is currently jailed", + }); + } else if ( + missedBlocks > 0 && + missedBlocks / signedBlocksWindow > 0.05 + ) { + alerts.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + severity: "HIGH", + reason: `Missed ${missedBlocks}/${signedBlocksWindow} blocks (${((missedBlocks / signedBlocksWindow) * 100).toFixed(2)}%)`, + }); + } + if (stakePct >= config.validator.criticalConcentrationPct) { + alerts.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + severity: "CRITICAL", + reason: `Single validator holds ${stakePct.toFixed(2)}% of bonded stake`, + }); + } + } + + scorecards.sort((a, b) => b.compositeScore - a.compositeScore); + + return { + scorecards, + alerts, + bondedUregen: pool.bonded_tokens, + }; + }, + + async decide(orientation: Orientation): Promise { + const report = await describePerformanceReport( + orientation.scorecards, + orientation.alerts, + orientation.bondedUregen + ); + + return { + report, + alertsToPublish: orientation.alerts, + scorecards: orientation.scorecards, + }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + // Persist every scorecard so the historical timeline is usable + // for future PoA transition assessments. + for (const card of decision.scorecards) { + store.saveScorecard({ + operatorAddress: card.operatorAddress, + compositeScore: card.compositeScore, + scorecard: JSON.stringify(card), + }); + saved++; + } + + await output({ + workflow: "WF-VM-01", + subjectId: "validator-set", + title: "Performance report", + content: decision.report, + alertLevel: + decision.alertsToPublish.some((a) => a.severity === "CRITICAL") + ? "CRITICAL" + : decision.alertsToPublish.length > 0 + ? "HIGH" + : "NORMAL", + timestamp: new Date(), + }); + if (decision.alertsToPublish.length > 0) alertsSent++; + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-004-validator-monitor/tsconfig.json b/agent-004-validator-monitor/tsconfig.json new file mode 100644 index 0000000..4a4a4f3 --- /dev/null +++ b/agent-004-validator-monitor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 110c4f1c31d88a0bc937bd8621b4221b795d929c Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:46:33 -0700 Subject: [PATCH 2/5] test(agent-004): add vitest unit tests for helper math MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling PR to the AGENT-003 unit tests (#99) and a follow-up to PR #81, which promised the unit tests as a "separate test-only PR so this PR stays a single-concern 'add the agent' change." Adds 33 unit tests across 2 test files covering every deterministic helper in the AGENT-004 workflows. The helpers are the core of the decentralization analysis surface — if they drift silently, the validator monitor produces misleading alerts or misses real concentration attacks. ## Changes ### Helper exports Five previously module-private helpers are now exported so the test files can import them: decentralization-monitor.ts: nakamotoCoefficient, giniIndex, topNSharePct, classifyHealth delegation-flow-analysis.ts: absBig The export is the only production-code change — no behavior change, no API rename. Module consumers are unchanged. ### Test files src/workflows/decentralization-monitor.test.ts (28 tests) nakamotoCoefficient — 8 tests - empty input / zero total returns 0 - single validator with entire stake → 1 - top validator > 33.4% → 1 - top validator exactly 33.4% (334/1000) → 2 (pins the STRICT `> threshold` predicate — a refactor that changes > to >= would silently produce Nakamoto = 1 here) - top two combined clear threshold → 2 - ten equal validators → 4 - degenerate case when total > sum of list giniIndex — 7 tests - empty / single-element → 0 - perfect equality → 0 - unequal distribution > 0 - maximally unequal (one holds everything) → approaches 1 - all-zeros → 0 (cumulative guard) - monotonicity: more inequality → higher Gini topNSharePct — 6 tests - zero total → 0 - top 1, top 3 cumulative share - n > array length → 100% - n = array length → 100% - two-decimal precision classifyHealth — 7 tests - HEALTHY baseline - CRITICAL on Nakamoto floor - CRITICAL on single-validator concentration - WARNING on Nakamoto warning floor - WARNING on Gini ceiling - WARNING on single-validator warning concentration - CRITICAL wins over WARNING when thresholds overlap src/workflows/delegation-flow-analysis.test.ts (5 tests) absBig — 5 tests - zero - positive inputs - negative inputs - values beyond Number.MAX_SAFE_INTEGER (2^53 + 1) — critical because AGENT-004 works in uregen and 221M REGEN is 2.21e14 uregen, close to the unsafe-integer boundary - idempotence ### Vitest setup Same structure as AGENT-003's test PR (#99): vitest.config.ts — standard config, node env package.json — adds "test" / "test:watch" + vitest ^2.1.0 tsconfig.json — excludes *.test.ts from prod typecheck .gitignore — adds *.db-shm and *.db-wal ## Validation $ cd agent-004-validator-monitor && npm test Test Files 2 passed (2) Tests 33 passed (33) $ cd agent-004-validator-monitor && npx tsc --noEmit (exit 0) Note: the tests import decentralization-monitor.ts, which in turn imports store.ts at the top level. The store constructor opens a SQLite database on import. If a prior test run left stale WAL lock files on disk, the next run fails with "database is locked". The .gitignore update prevents those lock files from landing in a PR; developers running tests locally may need to rm -f agent-004.db-shm agent-004.db-wal once if they hit the lock. ## Scope Does NOT touch the OODA loops, the Claude narrative layer, the LCD client, the SQLite store, or any output formatting. Tests cover pure functions only. - Lands in: `agent-004-validator-monitor/` - Changes: 33 unit tests + vitest setup + 5 helper exports - Validate: `cd agent-004-validator-monitor && npm test` ## PR relationship Based on PR #81's branch. If #81 merges first, this PR rebases cleanly. Sibling PR to #99 (AGENT-003 unit tests) — the two follow an identical structure and review together better than separately. --- agent-004-validator-monitor/.gitignore | 2 + agent-004-validator-monitor/package-lock.json | 2703 +++++++++++++---- agent-004-validator-monitor/package.json | 5 +- .../decentralization-monitor.test.ts | 173 ++ .../src/workflows/decentralization-monitor.ts | 8 +- .../delegation-flow-analysis.test.ts | 34 + .../src/workflows/delegation-flow-analysis.ts | 2 +- agent-004-validator-monitor/tsconfig.json | 2 +- agent-004-validator-monitor/vitest.config.ts | 8 + 9 files changed, 2271 insertions(+), 666 deletions(-) create mode 100644 agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts create mode 100644 agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts create mode 100644 agent-004-validator-monitor/vitest.config.ts diff --git a/agent-004-validator-monitor/.gitignore b/agent-004-validator-monitor/.gitignore index c44c9e2..467cd39 100644 --- a/agent-004-validator-monitor/.gitignore +++ b/agent-004-validator-monitor/.gitignore @@ -1,4 +1,6 @@ node_modules/ dist/ *.db +*.db-shm +*.db-wal .env diff --git a/agent-004-validator-monitor/package-lock.json b/agent-004-validator-monitor/package-lock.json index 8807065..bbd70f5 100644 --- a/agent-004-validator-monitor/package-lock.json +++ b/agent-004-validator-monitor/package-lock.json @@ -15,7 +15,8 @@ "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "tsx": "^4.19.0", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "vitest": "^2.1.0" }, "engines": { "node": ">=20.0.0" @@ -493,6 +494,363 @@ "node": ">=18" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -503,6 +861,13 @@ "@types/node": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -522,35 +887,158 @@ "form-data": "^4.0.4" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, "license": "MIT", "dependencies": { - "event-target-shim": "^5.0.0" + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=6.5" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, "license": "MIT", "dependencies": { - "humanize-ms": "^1.2.1" + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -572,874 +1060,1754 @@ ], "license": "MIT" }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "semver": "^7.3.5" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, "engines": { - "node": ">= 0.8" + "node": ">=10.5.0" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "4.x || >=6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">= 14.16" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 6" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, "bin": { - "esbuild": "bin/esbuild" + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, "license": "MIT" }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" }, "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, "license": "MIT" }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, "engines": { - "node": ">= 12.20" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "safe-buffer": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": "*" } }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">=14.17" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">= 0.4" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">= 0.4" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/vitest" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 0.4" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" + "node": ">=12" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.5.0" + "node": ">=12" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=12" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" + "node": ">=12" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" ], - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, "bin": { - "tsx": "dist/cli.mjs" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" }, "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" }, - "engines": { - "node": "*" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "vitest": "vitest.mjs" }, "engines": { - "node": ">=14.17" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -1465,6 +2833,23 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/agent-004-validator-monitor/package.json b/agent-004-validator-monitor/package.json index fd10636..7df6a40 100644 --- a/agent-004-validator-monitor/package.json +++ b/agent-004-validator-monitor/package.json @@ -9,6 +9,8 @@ "start": "node --import tsx src/index.ts", "dev": "node --import tsx --watch src/index.ts", "analyze": "node --import tsx src/index.ts --once", + "test": "vitest run", + "test:watch": "vitest", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -19,7 +21,8 @@ "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "tsx": "^4.19.0", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "vitest": "^2.1.0" }, "engines": { "node": ">=20.0.0" diff --git a/agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts b/agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts new file mode 100644 index 0000000..537b430 --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect } from "vitest"; +import { + nakamotoCoefficient, + giniIndex, + topNSharePct, + classifyHealth, +} from "./decentralization-monitor.js"; +import { config } from "../config.js"; + +// ============================================================ +// nakamotoCoefficient — smallest set of validators whose cumulative +// share strictly exceeds 33.4% of the total bonded stake. +// ============================================================ + +describe("nakamotoCoefficient", () => { + it("returns 0 for empty input", () => { + expect(nakamotoCoefficient([], 0n)).toBe(0); + expect(nakamotoCoefficient([], 100n)).toBe(0); + }); + + it("returns 0 when total is zero", () => { + expect(nakamotoCoefficient([10n, 20n], 0n)).toBe(0); + }); + + it("returns 1 when a single validator holds the entire stake", () => { + expect(nakamotoCoefficient([1000n], 1000n)).toBe(1); + }); + + it("returns 1 when a single validator holds more than 33.4%", () => { + // Top validator has 40% (400 out of 1000) — a single validator can halt. + expect(nakamotoCoefficient([400n, 300n, 200n, 100n], 1000n)).toBe(1); + }); + + it("returns 2 when the top validator holds exactly 33.4%", () => { + // 334/1000 = 33.4% — the strict-inequality predicate `> threshold` + // means the top validator alone does not clear the line. Need a + // second validator. The SPEC defines Nakamoto as the SMALLEST n + // whose cumulative share STRICTLY EXCEEDS 33.4%. + expect(nakamotoCoefficient([334n, 333n, 333n], 1000n)).toBe(2); + }); + + it("returns 2 when the top two combined exceed 33.4%", () => { + // Top two: 200+180 = 380, exceeds 334 (threshold for 33.4% of 1000) + expect(nakamotoCoefficient([200n, 180n, 170n, 150n, 150n, 150n], 1000n)).toBe(2); + }); + + it("returns a larger N when stake is distributed evenly", () => { + // 10 validators, each with 100 out of 1000 total. + // Cumulative share reaches 33.4% between the 3rd and 4th validator + // (3*100 = 300 < 334, 4*100 = 400 > 334). So Nakamoto = 4. + const tokens = Array(10).fill(100n); + expect(nakamotoCoefficient(tokens, 1000n)).toBe(4); + }); + + it("returns the length when no subset exceeds the threshold", () => { + // Degenerate: total is larger than the sum of the list (would not + // happen in practice, but the guard clamps to the array length). + expect(nakamotoCoefficient([10n, 10n], 1000n)).toBe(2); + }); +}); + +// ============================================================ +// giniIndex — textbook Gini on an ascending-sorted array +// ============================================================ + +describe("giniIndex", () => { + it("returns 0 for empty input", () => { + expect(giniIndex([])).toBe(0); + }); + + it("returns 0 for a single-element distribution", () => { + expect(giniIndex([100])).toBe(0); + }); + + it("returns 0 for perfect equality", () => { + expect(giniIndex([100, 100, 100, 100])).toBe(0); + expect(giniIndex([5, 5, 5, 5, 5, 5])).toBe(0); + }); + + it("returns a positive number for unequal distribution", () => { + expect(giniIndex([10, 20, 30, 40])).toBeGreaterThan(0); + }); + + it("approaches 1 for maximally unequal distribution", () => { + // One player has everything, others have nothing + const gini = giniIndex([0, 0, 0, 0, 100]); + expect(gini).toBeGreaterThan(0.6); + expect(gini).toBeLessThan(1); + }); + + it("returns 0 when the cumulative sum is zero (all zeros)", () => { + expect(giniIndex([0, 0, 0])).toBe(0); + }); + + it("is monotonic — more inequality, higher Gini", () => { + const nearEqual = giniIndex([95, 96, 97, 98, 99, 100]); + const moreUnequal = giniIndex([10, 20, 30, 40, 50, 60]); + expect(moreUnequal).toBeGreaterThan(nearEqual); + }); +}); + +// ============================================================ +// topNSharePct — cumulative share of the top N +// ============================================================ + +describe("topNSharePct", () => { + it("returns 0 when total is zero", () => { + expect(topNSharePct([10n, 20n], 0n, 2)).toBe(0); + }); + + it("returns the correct share for the single largest validator", () => { + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 1)).toBe(40); + }); + + it("returns the correct share for the top 3", () => { + // 400 + 300 + 200 = 900 of 1000 = 90% + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 3)).toBe(90); + }); + + it("returns 100% when n exceeds the array length", () => { + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 100)).toBe(100); + }); + + it("returns 100% when n equals the array length", () => { + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 4)).toBe(100); + }); + + it("has two-decimal precision on the returned percentage", () => { + // 333 / 1000 = 33.3% + expect(topNSharePct([333n, 333n, 334n], 1000n, 1)).toBe(33.3); + }); +}); + +// ============================================================ +// classifyHealth — composite health tier +// ============================================================ + +describe("classifyHealth", () => { + const v = config.validator; + + it("returns HEALTHY for high Nakamoto, low Gini, low concentration", () => { + expect(classifyHealth(20, 0.3, 10)).toBe("HEALTHY"); + }); + + it("returns CRITICAL when Nakamoto is at or below the critical floor", () => { + expect(classifyHealth(v.nakamotoCriticalFloor, 0.3, 10)).toBe("CRITICAL"); + expect(classifyHealth(v.nakamotoCriticalFloor - 1, 0.3, 10)).toBe("CRITICAL"); + }); + + it("returns CRITICAL when a single validator exceeds the critical concentration", () => { + expect(classifyHealth(20, 0.3, v.criticalConcentrationPct)).toBe("CRITICAL"); + expect(classifyHealth(20, 0.3, v.criticalConcentrationPct + 5)).toBe("CRITICAL"); + }); + + it("returns WARNING when Nakamoto is at the warning floor but above critical", () => { + expect(classifyHealth(v.nakamotoWarningFloor, 0.3, 10)).toBe("WARNING"); + }); + + it("returns WARNING when Gini exceeds the warning ceiling", () => { + expect(classifyHealth(20, v.giniWarningCeiling, 10)).toBe("WARNING"); + expect(classifyHealth(20, v.giniWarningCeiling + 0.05, 10)).toBe("WARNING"); + }); + + it("returns WARNING when a single validator is at the warning concentration", () => { + expect(classifyHealth(20, 0.3, v.warningConcentrationPct)).toBe("WARNING"); + }); + + it("CRITICAL wins over WARNING when multiple thresholds trip", () => { + expect( + classifyHealth(v.nakamotoCriticalFloor, v.giniWarningCeiling, v.warningConcentrationPct) + ).toBe("CRITICAL"); + }); +}); diff --git a/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts index 511a060..6c42376 100644 --- a/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts +++ b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts @@ -49,7 +49,7 @@ interface Actions { * bonded stake strictly exceeds 33.4%. Consensus breaks at 2/3+1, so * the canonical Cosmos-era Nakamoto coefficient uses 33% as the * halt-threshold proxy. */ -function nakamotoCoefficient(sortedDescTokens: bigint[], total: bigint): number { +export function nakamotoCoefficient(sortedDescTokens: bigint[], total: bigint): number { if (total === 0n || sortedDescTokens.length === 0) return 0; const threshold = (total * 334n) / 1000n; // 33.4% let cumulative = 0n; @@ -63,7 +63,7 @@ function nakamotoCoefficient(sortedDescTokens: bigint[], total: bigint): number /** Gini index of a distribution. 0 = perfect equality, 1 = perfect * inequality. Operates on ascending-sorted numbers and uses the * textbook formula. */ -function giniIndex(sortedAscTokens: number[]): number { +export function giniIndex(sortedAscTokens: number[]): number { const n = sortedAscTokens.length; if (n === 0) return 0; let sum = 0; @@ -76,7 +76,7 @@ function giniIndex(sortedAscTokens: number[]): number { return Math.abs(sum / (n * cumulative)); } -function topNSharePct( +export function topNSharePct( sortedDescTokens: bigint[], total: bigint, n: number @@ -89,7 +89,7 @@ function topNSharePct( return Number((acc * 10000n) / total) / 100; } -function classifyHealth( +export function classifyHealth( nakamoto: number, gini: number, largestSharePct: number diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts new file mode 100644 index 0000000..8341e0e --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { absBig } from "./delegation-flow-analysis.js"; + +// ============================================================ +// absBig — BigInt absolute value +// ============================================================ + +describe("absBig", () => { + it("returns 0 for 0n", () => { + expect(absBig(0n)).toBe(0n); + }); + + it("returns the value for positive inputs", () => { + expect(absBig(1n)).toBe(1n); + expect(absBig(1_000_000_000_000_000n)).toBe(1_000_000_000_000_000n); + }); + + it("returns the negated value for negative inputs", () => { + expect(absBig(-1n)).toBe(1n); + expect(absBig(-1_000_000_000_000_000n)).toBe(1_000_000_000_000_000n); + }); + + it("handles values beyond JS number safe integer range", () => { + // 2^53 + 1 — beyond Number.MAX_SAFE_INTEGER, only BigInt can represent exactly. + const huge = 9_007_199_254_740_993n; + expect(absBig(huge)).toBe(huge); + expect(absBig(-huge)).toBe(huge); + }); + + it("is idempotent on positive inputs", () => { + expect(absBig(absBig(42n))).toBe(42n); + expect(absBig(absBig(-42n))).toBe(42n); + }); +}); diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts index 4eef1eb..6bda415 100644 --- a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts @@ -48,7 +48,7 @@ interface Actions { alertsSent: number; } -function absBig(x: bigint): bigint { +export function absBig(x: bigint): bigint { return x < 0n ? -x : x; } diff --git a/agent-004-validator-monitor/tsconfig.json b/agent-004-validator-monitor/tsconfig.json index 4a4a4f3..e6a11e8 100644 --- a/agent-004-validator-monitor/tsconfig.json +++ b/agent-004-validator-monitor/tsconfig.json @@ -16,5 +16,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "vitest.config.ts"] } diff --git a/agent-004-validator-monitor/vitest.config.ts b/agent-004-validator-monitor/vitest.config.ts new file mode 100644 index 0000000..ce36a74 --- /dev/null +++ b/agent-004-validator-monitor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + }, +}); From 962e0ceddb6233d005bd541c3338272b32fcca97 Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:08:18 -0700 Subject: [PATCH 3/5] feat(agent-004): real delegation tx-stream for WF-VM-02 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the token-delta MVP proxy with a real staking tx-search client that reads recent MsgDelegate, MsgUndelegate, and MsgBeginRedelegate events from the Cosmos LCD. Closes the follow-up documented in PR #81's design-decision #2. ## What changes in the workflow The observe phase no longer snapshots `validator.tokens` or consults the previous snapshot. Instead: const events = await ledger.getRecentDelegationTxs(200); The orient phase aggregates events per-validator via a new pure function `aggregateEventsToFlows`, which handles three rules: - delegate → inflow to event.validator - undelegate → outflow from event.validator - redelegate → outflow from event.sourceValidator, inflow to event.validator (destination) `summarizeFlows` (also new and exported for tests) derives the totals, whale count, top inflow, and top outflow from the flow list. Neither function touches the store — the old per-cycle token snapshot is no longer needed. The monikers for the narrative layer are still backfilled from a single `ledger.getValidators()` call inside the orient phase. ## What changes in the ledger client New methods on LedgerClient: - `getRecentDelegationTxs(limit)` — queries the LCD tx-search endpoint once per staking message type (three type URLs total) and flattens the results into a single DelegationEvent list. Per-type failures are isolated: if the MsgUndelegate query fails for any reason, the MsgDelegate and MsgBeginRedelegate results still come through. - `parseDelegationEventsFromTx(tx)` — public pure function. Walks events at both `logs[].events[]` and top-level `tx.events[]` positions for cross-SDK compatibility. Matches three Cosmos SDK event types: `delegate`, `unbond`, and `redelegate`. Extracts the delegator address from the positionally-corresponding `message` event sender. - New helper `parseCoinAmount(raw)` extracts the numeric prefix from Cosmos coin-amount strings like "1000uregen", returning the numeric part as a string for BigInt-safe downstream consumption. ## New types - `DelegationEvent` — a single on-chain staking event with txHash, eventType, delegator, validator, sourceValidator (only set for redelegate), amountUregen, and occurredAt. The DelegationFlow type is preserved for backward compatibility with the narrative layer. ## New tests — 22 total (55 total across 3 files, up from 33 in #100) ### src/ledger.test.ts (9 new tests) - empty tx - MsgDelegate extraction with sender from message event - MsgUndelegate extraction via the `unbond` event type - MsgBeginRedelegate with source + destination validators - batched: 3 staking events in one tx with positional sender matching - missing validator attribute ignored - malformed amount attribute ignored - coin-amount format "uregen" parsed correctly - events read from tx.events[] alongside logs[].events[] ### src/workflows/delegation-flow-analysis.test.ts (13 new tests, on top of the existing 5 for absBig) - aggregateEventsToFlows: - empty - single delegate → inflow - single undelegate → outflow - redelegate → two flows (source + destination) - net delegate + undelegate on same validator - zero net delta skipped - whale threshold tagging - zero/negative amount skipped - non-numeric amount skipped - summarizeFlows: - zero totals on empty - inflow / outflow / net sum correctness - top inflow + top outflow identification - whale count separate from total ## What this unlocks The old MVP proxy couldn't: - Distinguish delegate from undelegate from redelegate. A validator losing 100K stake could be a pure outflow (undelegate) or a redelegate to a different validator, but the proxy saw the same delta number either way. - Attribute flows to a specific delegator address. - Capture intra-cycle movements that net to zero (A delegates, B undelegates same amount within a minute — the old proxy reported "no change" and missed both events). - Produce a reliable audit trail linking back to real tx hashes. The new implementation fixes all four. ## Scope Does NOT touch WF-VM-01 (performance tracking) or WF-VM-03 (decentralization monitor). The bech32 operator→delegator conversion for governance participation scoring is a separate follow-up (the m014 governance score is still MVP-zero after this PR). - Lands in: `agent-004-validator-monitor/` - Changes: new tx-search client + new DelegationEvent type + rewritten WF-VM-02 observe+orient phases + 22 new tests - Validate: `cd agent-004-validator-monitor && npm test && npx tsc --noEmit` ## PR relationship Based on PR #100 (AGENT-004 unit tests) which is based on PR #81 (AGENT-004 initial implementation). Sibling to PR #103 (AGENT-003 MsgRetire tx-stream). The two real-tx-stream PRs close the MVP-proxy column for both market-monitor and validator-monitor in the same session. Refs `phase-2/2.2-agentic-workflows.md` §WF-VM-02 Refs PR #81's design decision #2 (MVP token-delta proxy → real tx-stream follow-up) --- agent-004-validator-monitor/README.md | 2 +- .../src/ledger.test.ts | 255 ++++++++++++++++++ agent-004-validator-monitor/src/ledger.ts | 154 +++++++++++ agent-004-validator-monitor/src/types.ts | 18 ++ .../delegation-flow-analysis.test.ts | 210 ++++++++++++++- .../src/workflows/delegation-flow-analysis.ts | 245 +++++++++++------ 6 files changed, 794 insertions(+), 90 deletions(-) create mode 100644 agent-004-validator-monitor/src/ledger.test.ts diff --git a/agent-004-validator-monitor/README.md b/agent-004-validator-monitor/README.md index 4a3dc1e..5a860f9 100644 --- a/agent-004-validator-monitor/README.md +++ b/agent-004-validator-monitor/README.md @@ -93,7 +93,7 @@ Composite score is **0-1000**, computed deterministically in `performance-tracki 1. **Deterministic numbers, narrative-only LLM calls.** All scoring, Nakamoto and Gini computation, health classification, and whale detection happen locally in plain TypeScript. Claude is only invoked to write the report. Keeps the agent cheap, reproducible, and auditable. -2. **Token-delta as delegation source for the MVP.** WF-VM-02 snapshots each validator's `tokens` field every cycle and derives flows from the delta against the previous snapshot. A follow-up PR will plug in a real `MsgDelegate` / `MsgUndelegate` / `MsgRedelegate` tx-stream client. The code path swaps cleanly once real events are available. +2. **Real MsgDelegate / MsgUndelegate / MsgBeginRedelegate tx-stream.** WF-VM-02 reads recent staking events from the Cosmos LCD `tx-search` endpoint, filtered by message type URL, and parses them into `DelegationEvent` records. Each tx can carry multiple events (batched operations); the parser walks `logs[].events[]` and the flattened `tx.events[]` for cross-SDK compatibility. The aggregator groups events per-validator: delegate → inflow, undelegate → outflow, redelegate → source outflow + destination inflow. Earlier drafts used a token-delta proxy against `validator.tokens` snapshots; the current implementation produces events with delegator identity and bit-exact amounts rather than synthesized deltas. 3. **Governance participation scoring is MVP-zero.** The operator→delegator bech32 conversion (required to fetch per-validator votes via `/proposals/{id}/votes/{voter}`) is deferred to a follow-up PR. All validators currently receive a 0 governance score, so relative composite scores still surface real differences in uptime and stability without punishing anyone asymmetrically. A separate PR will add the bech32 conversion and full vote fetching. diff --git a/agent-004-validator-monitor/src/ledger.test.ts b/agent-004-validator-monitor/src/ledger.test.ts new file mode 100644 index 0000000..4c7a594 --- /dev/null +++ b/agent-004-validator-monitor/src/ledger.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect } from "vitest"; +import { LedgerClient } from "./ledger.js"; + +// ============================================================ +// parseDelegationEventsFromTx — staking event extraction +// ============================================================ + +describe("LedgerClient.parseDelegationEventsFromTx", () => { + const client = new LedgerClient(); + + it("returns empty array for a tx with no events", () => { + const tx = { txhash: "tx-empty", timestamp: "2026-02-18T12:00:00Z", logs: [] }; + expect(client.parseDelegationEventsFromTx(tx)).toEqual([]); + }); + + it("extracts a MsgDelegate event with the sender as delegator", () => { + const tx = { + txhash: "tx-delegate", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [ + { key: "action", value: "/cosmos.staking.v1beta1.MsgDelegate" }, + { key: "sender", value: "regen1delegator1" }, + ], + }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1alpha" }, + { key: "amount", value: "1000uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ + txHash: "tx-delegate", + eventType: "delegate", + delegator: "regen1delegator1", + validator: "regenvaloper1alpha", + sourceValidator: null, + amountUregen: "1000", + }); + }); + + it("extracts a MsgUndelegate event from the `unbond` type", () => { + const tx = { + txhash: "tx-undelegate", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [ + { key: "action", value: "/cosmos.staking.v1beta1.MsgUndelegate" }, + { key: "sender", value: "regen1delegator2" }, + ], + }, + { + type: "unbond", + attributes: [ + { key: "validator", value: "regenvaloper1beta" }, + { key: "amount", value: "500uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.eventType).toBe("undelegate"); + expect(out[0]!.validator).toBe("regenvaloper1beta"); + expect(out[0]!.amountUregen).toBe("500"); + }); + + it("extracts a MsgBeginRedelegate event with source + destination", () => { + const tx = { + txhash: "tx-redelegate", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [ + { key: "action", value: "/cosmos.staking.v1beta1.MsgBeginRedelegate" }, + { key: "sender", value: "regen1delegator3" }, + ], + }, + { + type: "redelegate", + attributes: [ + { key: "source_validator", value: "regenvaloper1src" }, + { key: "destination_validator", value: "regenvaloper1dst" }, + { key: "amount", value: "2000uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.eventType).toBe("redelegate"); + expect(out[0]!.sourceValidator).toBe("regenvaloper1src"); + expect(out[0]!.validator).toBe("regenvaloper1dst"); + expect(out[0]!.amountUregen).toBe("2000"); + }); + + it("parses multiple staking events in a single batched tx", () => { + // Two delegations and one undelegation in one tx, with positional + // sender matching — sender[0] → first staking event, sender[1] → + // second, sender[2] → third. + const tx = { + txhash: "tx-batched", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d1" }] }, + { type: "message", attributes: [{ key: "sender", value: "regen1d2" }] }, + { type: "message", attributes: [{ key: "sender", value: "regen1d3" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1a" }, + { key: "amount", value: "100uregen" }, + ], + }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1b" }, + { key: "amount", value: "200uregen" }, + ], + }, + { + type: "unbond", + attributes: [ + { key: "validator", value: "regenvaloper1c" }, + { key: "amount", value: "50uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(3); + expect(out[0]!.delegator).toBe("regen1d1"); + expect(out[1]!.delegator).toBe("regen1d2"); + expect(out[2]!.delegator).toBe("regen1d3"); + }); + + it("ignores delegate events with missing validator attribute", () => { + const tx = { + txhash: "tx-missing-validator", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [{ key: "sender", value: "regen1d" }], + }, + { + type: "delegate", + attributes: [{ key: "amount", value: "1000uregen" }], + }, + ], + }, + ], + }; + expect(client.parseDelegationEventsFromTx(tx)).toEqual([]); + }); + + it("ignores delegate events with malformed amount attribute", () => { + const tx = { + txhash: "tx-bad-amount", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [{ key: "sender", value: "regen1d" }], + }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1x" }, + { key: "amount", value: "no-numbers-here" }, + ], + }, + ], + }, + ], + }; + expect(client.parseDelegationEventsFromTx(tx)).toEqual([]); + }); + + it("parses the amount prefix when the denom follows the number", () => { + // The Cosmos SDK coin-amount format is "uregen". The parser + // extracts the numeric prefix and drops the denom. + const tx = { + txhash: "tx-coin-format", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1x" }, + { key: "amount", value: "12345678uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out[0]!.amountUregen).toBe("12345678"); + }); + + it("reads events from tx.events[] alongside logs[].events[]", () => { + const tx = { + txhash: "tx-flat", + timestamp: "2026-02-18T12:00:00Z", + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1y" }, + { key: "amount", value: "42uregen" }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.amountUregen).toBe("42"); + }); +}); diff --git a/agent-004-validator-monitor/src/ledger.ts b/agent-004-validator-monitor/src/ledger.ts index 8dce42a..612c8a6 100644 --- a/agent-004-validator-monitor/src/ledger.ts +++ b/agent-004-validator-monitor/src/ledger.ts @@ -6,8 +6,23 @@ import type { StakingPool, Proposal, Vote, + DelegationEvent, } from "./types.js"; +/** + * Parse a Cosmos SDK coin-amount attribute into a uregen string. + * The staking event attributes carry amounts like `"1000uregen"` — + * the numeric part followed by the denom. Strip the denom and + * return the numeric prefix as a string so BigInt downstream can + * consume it losslessly. Returns null on bad input. + */ +function parseCoinAmount(raw: string | null): string | null { + if (!raw) return null; + const m = raw.match(/^(\d+)/); + if (!m) return null; + return m[1] ?? null; +} + /** * Regen Ledger LCD (REST) client — staking, slashing, gov endpoints. * @@ -123,6 +138,145 @@ export class LedgerClient { } } + // ── Tx-search: staking delegation events ──────────────────── + // + // Pulls recent tx responses for each of the three staking message + // types and parses them into DelegationEvent records. Each call + // hits a different tx-search event filter because the LCD only + // supports one `events=` filter per request. + + async getRecentDelegationTxs(limit = 100): Promise { + const typeUrls = [ + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + ]; + + const results: DelegationEvent[] = []; + + for (const typeUrl of typeUrls) { + try { + const params = new URLSearchParams(); + params.set("events", `message.action='${typeUrl}'`); + params.set("pagination.limit", String(limit)); + params.set("pagination.reverse", "true"); + params.set("order_by", "ORDER_BY_DESC"); + + const data = await this.get(`/cosmos/tx/v1beta1/txs?${params.toString()}`); + const txResponses = (data.tx_responses || []) as Array>; + + for (const tx of txResponses) { + results.push(...this.parseDelegationEventsFromTx(tx)); + } + } catch { + // Per-type-url failure is isolated — the other two still run. + // An off-chain aggregator that loses one of three queries is + // still better than falling back to the token-delta proxy. + } + } + + return results; + } + + /** + * Parse a single Cosmos tx_response into zero or more + * DelegationEvent records. Public so unit tests can feed it + * synthetic inputs. Matches three Cosmos SDK event types: + * + * - `delegate` (MsgDelegate) + * - `unbond` (MsgUndelegate) + * - `redelegate` (MsgBeginRedelegate) + * + * Also reads the `message` event to extract the delegator address + * (sender) because the staking events themselves don't carry it. + * The association is positional: the Nth staking event in a tx + * maps to the Nth message event's sender. + */ + parseDelegationEventsFromTx(tx: Record): DelegationEvent[] { + const txHash = typeof tx.txhash === "string" ? tx.txhash : ""; + const timestamp = + typeof tx.timestamp === "string" ? tx.timestamp : new Date().toISOString(); + + const collected: Array> = []; + const logs = (tx.logs || []) as Array>; + for (const log of logs) { + collected.push(...((log.events || []) as Array>)); + } + collected.push(...((tx.events || []) as Array>)); + + // Sender addresses from message events, in order of appearance. + const senders: string[] = []; + for (const ev of collected) { + if (typeof ev.type === "string" && ev.type === "message") { + const attributes = (ev.attributes || []) as Array<{ key: string; value: string }>; + const sender = attributes.find((a) => a.key === "sender"); + if (sender) senders.push(sender.value); + } + } + + const results: DelegationEvent[] = []; + let senderIdx = 0; + + const nextSender = (): string => { + const s = senders[senderIdx] ?? ""; + senderIdx++; + return s; + }; + + for (const ev of collected) { + const type = typeof ev.type === "string" ? ev.type : ""; + const attributes = (ev.attributes || []) as Array<{ key: string; value: string }>; + const attr = (k: string): string | null => { + const hit = attributes.find((a) => a.key === k); + return hit ? hit.value : null; + }; + + if (type === "delegate") { + const validator = attr("validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!validator || amount === null) continue; + results.push({ + txHash, + eventType: "delegate", + delegator: nextSender(), + validator, + sourceValidator: null, + amountUregen: amount, + occurredAt: timestamp, + }); + } else if (type === "unbond") { + const validator = attr("validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!validator || amount === null) continue; + results.push({ + txHash, + eventType: "undelegate", + delegator: nextSender(), + validator, + sourceValidator: null, + amountUregen: amount, + occurredAt: timestamp, + }); + } else if (type === "redelegate") { + const src = attr("source_validator") ?? ""; + const dst = attr("destination_validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!src || !dst || amount === null) continue; + results.push({ + txHash, + eventType: "redelegate", + delegator: nextSender(), + validator: dst, + sourceValidator: src, + amountUregen: amount, + occurredAt: timestamp, + }); + } + } + + return results; + } + // ── Connectivity check ────────────────────────────────────── async checkConnection(): Promise<{ blockHeight: string }> { diff --git a/agent-004-validator-monitor/src/types.ts b/agent-004-validator-monitor/src/types.ts index 0d0d4d0..4fd38f4 100644 --- a/agent-004-validator-monitor/src/types.ts +++ b/agent-004-validator-monitor/src/types.ts @@ -117,6 +117,24 @@ export interface PerformanceAlert { reason: string; } +/** A single on-chain staking event parsed from a MsgDelegate / + * MsgUndelegate / MsgBeginRedelegate transaction. WF-VM-02's + * observe phase harvests these directly from the LCD tx-search + * endpoint; the old token-delta proxy is no longer used. */ +export interface DelegationEvent { + txHash: string; + eventType: "delegate" | "undelegate" | "redelegate"; + delegator: string; + /** For delegate/undelegate, the operator address of the validator. + * For redelegate, the destination_validator (where stake moves TO). */ + validator: string; + /** Only set for redelegate. Otherwise null. */ + sourceValidator: string | null; + /** Amount in uregen as a string to preserve BigInt precision. */ + amountUregen: string; + occurredAt: string; +} + /** Delegation flow analysis (WF-VM-02) */ export interface DelegationFlow { operatorAddress: string; diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts index 8341e0e..19def0c 100644 --- a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from "vitest"; -import { absBig } from "./delegation-flow-analysis.js"; +import { + absBig, + aggregateEventsToFlows, + summarizeFlows, +} from "./delegation-flow-analysis.js"; +import type { DelegationEvent, DelegationFlow } from "../types.js"; // ============================================================ // absBig — BigInt absolute value @@ -21,7 +26,6 @@ describe("absBig", () => { }); it("handles values beyond JS number safe integer range", () => { - // 2^53 + 1 — beyond Number.MAX_SAFE_INTEGER, only BigInt can represent exactly. const huge = 9_007_199_254_740_993n; expect(absBig(huge)).toBe(huge); expect(absBig(-huge)).toBe(huge); @@ -32,3 +36,205 @@ describe("absBig", () => { expect(absBig(absBig(-42n))).toBe(42n); }); }); + +// ============================================================ +// aggregateEventsToFlows — group events into per-validator flows +// ============================================================ + +function mkEvent(overrides: Partial = {}): DelegationEvent { + return { + txHash: "tx-test", + eventType: "delegate", + delegator: "regen1delegator", + validator: "regenvaloper1alpha", + sourceValidator: null, + amountUregen: "100", + occurredAt: "2026-02-18T12:00:00Z", + ...overrides, + }; +} + +describe("aggregateEventsToFlows", () => { + it("returns empty for empty input", () => { + expect(aggregateEventsToFlows([])).toEqual([]); + }); + + it("aggregates a single delegate event into one inflow", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ amountUregen: "500", validator: "regenvaloper1alpha" }), + ]); + expect(flows).toHaveLength(1); + expect(flows[0]!.operatorAddress).toBe("regenvaloper1alpha"); + expect(flows[0]!.flowDirection).toBe("INFLOW"); + expect(flows[0]!.deltaUregen).toBe("500"); + }); + + it("aggregates a single undelegate event into one outflow", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ + eventType: "undelegate", + amountUregen: "500", + validator: "regenvaloper1alpha", + }), + ]); + expect(flows).toHaveLength(1); + expect(flows[0]!.flowDirection).toBe("OUTFLOW"); + expect(flows[0]!.deltaUregen).toBe("-500"); + expect(flows[0]!.deltaAbsUregen).toBe("500"); + }); + + it("splits a redelegate event into source outflow + destination inflow", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ + eventType: "redelegate", + amountUregen: "1000", + validator: "regenvaloper1dest", + sourceValidator: "regenvaloper1src", + }), + ]); + expect(flows).toHaveLength(2); + const src = flows.find((f) => f.operatorAddress === "regenvaloper1src")!; + const dst = flows.find((f) => f.operatorAddress === "regenvaloper1dest")!; + expect(src.flowDirection).toBe("OUTFLOW"); + expect(src.deltaUregen).toBe("-1000"); + expect(dst.flowDirection).toBe("INFLOW"); + expect(dst.deltaUregen).toBe("1000"); + }); + + it("nets delegate and undelegate on the same validator", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ validator: "regenvaloper1alpha", amountUregen: "1000" }), + mkEvent({ + eventType: "undelegate", + validator: "regenvaloper1alpha", + amountUregen: "300", + }), + ]); + expect(flows).toHaveLength(1); + expect(flows[0]!.deltaUregen).toBe("700"); + expect(flows[0]!.flowDirection).toBe("INFLOW"); + }); + + it("skips validators whose net delta is exactly zero", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ validator: "regenvaloper1alpha", amountUregen: "500" }), + mkEvent({ + eventType: "undelegate", + validator: "regenvaloper1alpha", + amountUregen: "500", + }), + ]); + expect(flows).toEqual([]); + }); + + it("flags whale-sized deltas above the configured threshold", () => { + // whaleDelegationUregen default is 100_000_000_000 (100K REGEN). + const flows = aggregateEventsToFlows([ + mkEvent({ + validator: "regenvaloper1whale", + amountUregen: "200000000000", + }), + mkEvent({ + validator: "regenvaloper1minnow", + amountUregen: "1000", + }), + ]); + const whale = flows.find((f) => f.operatorAddress === "regenvaloper1whale")!; + const minnow = flows.find((f) => f.operatorAddress === "regenvaloper1minnow")!; + expect(whale.isWhale).toBe(true); + expect(minnow.isWhale).toBe(false); + }); + + it("skips events with zero or negative amountUregen", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ amountUregen: "0" }), + mkEvent({ amountUregen: "-100" }), + ]); + expect(flows).toEqual([]); + }); + + it("skips events with a non-numeric amountUregen", () => { + const flows = aggregateEventsToFlows([mkEvent({ amountUregen: "not-a-number" })]); + expect(flows).toEqual([]); + }); +}); + +// ============================================================ +// summarizeFlows — summary math (inflow/outflow/net/whale/top) +// ============================================================ + +function mkFlow(overrides: Partial = {}): DelegationFlow { + return { + operatorAddress: "regenvaloper1test", + moniker: "Test", + previousTokens: "0", + currentTokens: "0", + deltaUregen: "0", + deltaAbsUregen: "0", + isWhale: false, + flowDirection: "FLAT", + capturedAt: "2026-02-18T12:00:00Z", + ...overrides, + }; +} + +describe("summarizeFlows", () => { + it("returns zero totals for empty input", () => { + const summary = summarizeFlows([]); + expect(summary.totalInflowUregen).toBe("0"); + expect(summary.totalOutflowUregen).toBe("0"); + expect(summary.netFlowUregen).toBe("0"); + expect(summary.validatorsWithFlow).toBe(0); + expect(summary.whaleFlowCount).toBe(0); + expect(summary.topInflow).toBeNull(); + expect(summary.topOutflow).toBeNull(); + }); + + it("sums inflows and outflows correctly", () => { + const summary = summarizeFlows([ + mkFlow({ deltaUregen: "1000", flowDirection: "INFLOW" }), + mkFlow({ deltaUregen: "500", flowDirection: "INFLOW" }), + mkFlow({ + deltaUregen: "-300", + deltaAbsUregen: "300", + flowDirection: "OUTFLOW", + }), + ]); + expect(summary.totalInflowUregen).toBe("1500"); + expect(summary.totalOutflowUregen).toBe("300"); + expect(summary.netFlowUregen).toBe("1200"); + expect(summary.validatorsWithFlow).toBe(3); + }); + + it("identifies the top inflow and top outflow", () => { + const summary = summarizeFlows([ + mkFlow({ + operatorAddress: "regenvaloper1big", + deltaUregen: "10000", + flowDirection: "INFLOW", + }), + mkFlow({ + operatorAddress: "regenvaloper1small", + deltaUregen: "100", + flowDirection: "INFLOW", + }), + mkFlow({ + operatorAddress: "regenvaloper1bigout", + deltaUregen: "-5000", + deltaAbsUregen: "5000", + flowDirection: "OUTFLOW", + }), + ]); + expect(summary.topInflow?.operatorAddress).toBe("regenvaloper1big"); + expect(summary.topOutflow?.operatorAddress).toBe("regenvaloper1bigout"); + }); + + it("counts whale flows separately from the total", () => { + const summary = summarizeFlows([ + mkFlow({ deltaUregen: "500000000000", isWhale: true, flowDirection: "INFLOW" }), + mkFlow({ deltaUregen: "300", isWhale: false, flowDirection: "INFLOW" }), + ]); + expect(summary.whaleFlowCount).toBe(1); + expect(summary.validatorsWithFlow).toBe(2); + }); +}); diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts index 6bda415..8746c69 100644 --- a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts @@ -1,11 +1,10 @@ import { LedgerClient } from "../ledger.js"; -import { store } from "../store.js"; import { config } from "../config.js"; import { describeDelegationFlows } from "../monitor.js"; import { output } from "../output.js"; import type { OODAWorkflow } from "../ooda.js"; import type { - Validator, + DelegationEvent, DelegationFlow, DelegationFlowSummary, AlertLevel, @@ -14,22 +13,32 @@ import type { /** * WF-VM-02: Delegation Flow Analysis * - * Trigger: MsgDelegate / MsgUndelegate / MsgRedelegate (proxied by - * per-cycle `validator.tokens` deltas until a real tx-stream lands) + * Trigger: MsgDelegate / MsgUndelegate / MsgBeginRedelegate (real + * on-chain events via the LCD tx-search endpoint) * Layer: 1 (Fully Automated) * * OODA: - * Observe — Snapshot current `tokens` per validator; pull the prior - * snapshot from SQLite and compute per-validator deltas. - * Orient — Aggregate inflow / outflow / net. Tag whale-sized - * movements. Pick top inflow and outflow. + * Observe — Fetch recent delegation events from the LCD tx-search + * endpoint for all three staking message types. + * Orient — Aggregate events per validator into DelegationFlow + * records. Compute inflow/outflow/net across the whole + * set. Tag whale-sized movements. Pick top inflow and + * outflow. * Decide — Generate flow summary via Claude. * Act — Persist, output, alert on whale activity. + * + * Previous versions of this workflow used a token-delta MVP proxy + * — snapshotting `validator.tokens` per cycle and computing the + * delta against the previous snapshot. That proxy worked but + * could not distinguish delegate vs undelegate vs redelegate, did + * not carry delegator identity, and missed all the intra-cycle + * movements. The current implementation uses the real MsgDelegate + * / MsgUndelegate / MsgBeginRedelegate tx stream, so every flow + * in the summary is backed by a real on-chain transaction. */ interface Observations { - validators: Validator[]; - previousTokensByOperator: Map; + events: DelegationEvent[]; } interface Orientation { @@ -52,6 +61,126 @@ export function absBig(x: bigint): bigint { return x < 0n ? -x : x; } +/** + * Aggregate a flat list of DelegationEvent records into + * per-validator DelegationFlow records. Exported so unit tests + * can feed it synthetic input without going through the observe + * phase. + * + * Rules: + * - `delegate` → inflow to `event.validator` + * - `undelegate` → outflow from `event.validator` + * - `redelegate` → outflow from `event.sourceValidator`, + * inflow to `event.validator` (destination) + */ +export function aggregateEventsToFlows( + events: DelegationEvent[], + capturedAt: string = new Date().toISOString() +): DelegationFlow[] { + // Per-validator running deltas in uregen, signed. + const deltaByValidator = new Map(); + + const addDelta = (validator: string, delta: bigint) => { + const prev = deltaByValidator.get(validator) ?? 0n; + deltaByValidator.set(validator, prev + delta); + }; + + for (const ev of events) { + let amt: bigint; + try { + amt = BigInt(ev.amountUregen); + } catch { + continue; + } + if (amt <= 0n) continue; + + if (ev.eventType === "delegate") { + addDelta(ev.validator, amt); + } else if (ev.eventType === "undelegate") { + addDelta(ev.validator, -amt); + } else if (ev.eventType === "redelegate") { + if (ev.sourceValidator) addDelta(ev.sourceValidator, -amt); + addDelta(ev.validator, amt); + } + } + + const whaleThreshold = BigInt(config.validator.whaleDelegationUregen); + const flows: DelegationFlow[] = []; + + for (const [operatorAddress, delta] of deltaByValidator) { + if (delta === 0n) continue; + const deltaAbs = absBig(delta); + flows.push({ + operatorAddress, + // DelegationFlow carries a moniker for the narrative layer, but + // the event stream only has validator operator addresses. The + // orient phase inside the workflow resolves monikers by looking + // up the validator set once per cycle; the aggregator here + // leaves moniker as the operator address and expects the caller + // to backfill. Callers that only care about the aggregate math + // can use this field unchanged. + moniker: operatorAddress, + // Previous/current token fields are preserved for backward + // compatibility with the DelegationFlow shape, but they no + // longer correspond to validator.tokens snapshots — they + // describe the delta window instead. + previousTokens: "0", + currentTokens: delta.toString(), + deltaUregen: delta.toString(), + deltaAbsUregen: deltaAbs.toString(), + isWhale: deltaAbs >= whaleThreshold, + flowDirection: delta > 0n ? "INFLOW" : "OUTFLOW", + capturedAt, + }); + } + + return flows; +} + +/** + * Summarize a list of DelegationFlow records into a + * DelegationFlowSummary. Exported so unit tests can pin the + * summary math independently of the aggregator. + */ +export function summarizeFlows( + flows: DelegationFlow[], + capturedAt: string = new Date().toISOString() +): DelegationFlowSummary { + let totalInflow = 0n; + let totalOutflow = 0n; + let whaleFlowCount = 0; + let topInflow: DelegationFlow | null = null; + let topOutflow: DelegationFlow | null = null; + + for (const f of flows) { + const delta = BigInt(f.deltaUregen); + if (delta > 0n) { + totalInflow += delta; + if (!topInflow || delta > BigInt(topInflow.deltaUregen)) { + topInflow = f; + } + } else if (delta < 0n) { + totalOutflow += -delta; + if (!topOutflow || -delta > BigInt(topOutflow.deltaAbsUregen)) { + topOutflow = f; + } + } + if (f.isWhale) whaleFlowCount++; + } + + return { + windowLabel: "recent tx-search window", + totalInflowUregen: totalInflow.toString(), + totalOutflowUregen: totalOutflow.toString(), + netFlowUregen: (totalInflow - totalOutflow).toString(), + validatorsWithFlow: flows.length, + whaleFlowCount, + topInflow, + topOutflow, + capturedAt, + }; +} + export function createDelegationFlowAnalysisWorkflow( ledger: LedgerClient ): OODAWorkflow { @@ -60,91 +189,33 @@ export function createDelegationFlowAnalysisWorkflow( name: "Delegation Flow Analysis", async observe(): Promise { - const validators = await ledger.getValidators(); - - const previousTokensByOperator = new Map(); - for (const v of validators) { - const prev = store.getPreviousTokenSnapshot(v.operator_address, false); - if (prev) previousTokensByOperator.set(v.operator_address, prev.tokens); - } - - // Write the new snapshots AFTER reading previous — we want the - // delta to be "previous cycle vs this cycle". - for (const v of validators) { - store.recordTokenSnapshot({ - operatorAddress: v.operator_address, - moniker: v.description.moniker, - tokens: v.tokens, - commissionRate: v.commission.commission_rates.rate, - jailed: v.jailed, - }); - } - - return { validators, previousTokensByOperator }; + // Pull recent delegation tx events via the LCD tx-search + // endpoint. The ledger client queries each of the three + // staking message types separately and flattens the results + // into a single DelegationEvent list. + const events = await ledger.getRecentDelegationTxs(200); + return { events }; }, async orient(obs: Observations): Promise { const capturedAt = new Date().toISOString(); - const flows: DelegationFlow[] = []; - const whaleThreshold = BigInt(config.validator.whaleDelegationUregen); - - for (const v of obs.validators) { - const previousStr = obs.previousTokensByOperator.get(v.operator_address); - if (!previousStr) continue; // First time we see this validator; nothing to diff. - - const previous = BigInt(previousStr); - const current = BigInt(v.tokens || "0"); - const delta = current - previous; - const deltaAbs = absBig(delta); - if (delta === 0n) continue; - - flows.push({ - operatorAddress: v.operator_address, - moniker: v.description.moniker, - previousTokens: previousStr, - currentTokens: v.tokens, - deltaUregen: delta.toString(), - deltaAbsUregen: deltaAbs.toString(), - isWhale: deltaAbs >= whaleThreshold, - flowDirection: delta > 0n ? "INFLOW" : delta < 0n ? "OUTFLOW" : "FLAT", - capturedAt, - }); - } - - let totalInflow = 0n; - let totalOutflow = 0n; - let whaleFlowCount = 0; - let topInflow: DelegationFlow | null = null; - let topOutflow: DelegationFlow | null = null; + const flows = aggregateEventsToFlows(obs.events, capturedAt); + // Backfill monikers from the current validator set. This is a + // single extra LCD call per cycle — cheap — and it lets the + // narrative layer show operator names instead of opaque + // bech32 addresses. + const validators = await ledger.getValidators(); + const monikers = new Map(); + for (const v of validators) { + monikers.set(v.operator_address, v.description.moniker); + } for (const f of flows) { - const delta = BigInt(f.deltaUregen); - if (delta > 0n) { - totalInflow += delta; - if (!topInflow || delta > BigInt(topInflow.deltaUregen)) { - topInflow = f; - } - } else if (delta < 0n) { - totalOutflow += -delta; - if (!topOutflow || -delta > BigInt(topOutflow.deltaAbsUregen)) { - topOutflow = f; - } - } - if (f.isWhale) whaleFlowCount++; + const moniker = monikers.get(f.operatorAddress); + if (moniker) f.moniker = moniker; } - const summary: DelegationFlowSummary = { - windowLabel: "since previous cycle", - totalInflowUregen: totalInflow.toString(), - totalOutflowUregen: totalOutflow.toString(), - netFlowUregen: (totalInflow - totalOutflow).toString(), - validatorsWithFlow: flows.length, - whaleFlowCount, - topInflow, - topOutflow, - capturedAt, - }; - + const summary = summarizeFlows(flows, capturedAt); return { flows, summary }; }, From 7d23fae8afd53a067d1e38b052f3591c1d981741 Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sat, 11 Apr 2026 14:54:09 -0700 Subject: [PATCH 4/5] =?UTF-8?q?fix(agent-004):=20address=20Gemini=20review?= =?UTF-8?q?=20=E2=80=94=20bech32,=20gini=20precision,=20OFFSET,=20loop,=20?= =?UTF-8?q?commission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini review feedback on PR #81: WF-VM-01 (performance-tracking): * Derive the validator's regenvalcons1… consensus address from its consensus pubkey (SHA256 of pubkey bytes → first 20 bytes → bech32 under the consensus HRP) and use that as the signingByConsAddrLike lookup key. The old code was joining a base64 pubkey string against a bech32 address, so the lookup always missed and every validator reported 0 missed blocks — uptime scored 100% across the board. * Hoist the trailing-window `sinceIso` out of the per-validator loop so we do not recompute the same Date arithmetic N times per cycle. * Drop the dead `operatorToAccountBech32` stub and use the real `operatorToDelegator` helper from the new bech32 module. WF-VM-03 (decentralization-monitor): * Gini index now converts the full uregen value to Number without pre-dividing by 1_000_000n. Integer division floored every validator with less than 1 REGEN to zero and discarded fractional REGEN for larger stakes, both of which distort the Gini. uregen fits inside Number.MAX_SAFE_INTEGER safely, so no precision is lost by keeping the full value. Store: * countCommissionChangesSince now checks whether a baseline row exists *before* the window and counts the in-window rows accordingly. The old `cnt - 1` path dropped a real commission change whenever the baseline read fell outside the window. * getLatestDecentralizationSnapshot uses `LIMIT 1` (no OFFSET). The caller runs in the `orient` phase before the current cycle's snapshot is written, so the newest row is the actual previous cycle's snapshot. `OFFSET 1` was skipping it and either comparing against the cycle before last or returning null on the second run. * Store constructor accepts an optional dbPath (defaulting to the on-disk DB file) so unit tests can pass `:memory:` without clobbering the shared DB or hitting the "database is locked" failure mode. New src/bech32.ts module: * consensusPubkeyToConsAddress, operatorToDelegator, delegatorToOperator built on the `bech32` npm package. Centralized so WF-VM-01 and the forthcoming WF-VM-02 real tx-stream (PR #104) share one implementation. * Added `bech32: ^2.0.0` dependency to agent-004/package.json. Main loop: * setInterval → recursive setTimeout so a slow cycle cannot overlap with the next tick. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent-004-validator-monitor/package-lock.json | 7 ++ agent-004-validator-monitor/package.json | 3 +- agent-004-validator-monitor/src/bech32.ts | 73 +++++++++++++++++++ agent-004-validator-monitor/src/index.ts | 27 +++++-- agent-004-validator-monitor/src/store.ts | 53 +++++++++++--- .../src/workflows/decentralization-monitor.ts | 8 +- .../src/workflows/performance-tracking.ts | 56 ++++++++------ 7 files changed, 185 insertions(+), 42 deletions(-) create mode 100644 agent-004-validator-monitor/src/bech32.ts diff --git a/agent-004-validator-monitor/package-lock.json b/agent-004-validator-monitor/package-lock.json index bbd70f5..6bbafda 100644 --- a/agent-004-validator-monitor/package-lock.json +++ b/agent-004-validator-monitor/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", + "bech32": "^2.0.0", "better-sqlite3": "^11.7.0" }, "devDependencies": { @@ -1060,6 +1061,12 @@ ], "license": "MIT" }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, "node_modules/better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", diff --git a/agent-004-validator-monitor/package.json b/agent-004-validator-monitor/package.json index 7df6a40..522244c 100644 --- a/agent-004-validator-monitor/package.json +++ b/agent-004-validator-monitor/package.json @@ -15,7 +15,8 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", - "better-sqlite3": "^11.7.0" + "better-sqlite3": "^11.7.0", + "bech32": "^2.0.0" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", diff --git a/agent-004-validator-monitor/src/bech32.ts b/agent-004-validator-monitor/src/bech32.ts new file mode 100644 index 0000000..af68209 --- /dev/null +++ b/agent-004-validator-monitor/src/bech32.ts @@ -0,0 +1,73 @@ +import { createHash } from "node:crypto"; +import { bech32 } from "bech32"; + +/** + * Cosmos bech32 derivation helpers. + * + * These are the minimum set of conversions the validator monitor + * needs to join across the three address spaces that Cosmos uses: + * + * - operator bech32 (`regenvaloper1…`) — governs the validator + * - delegator bech32 (`regen1…`) — votes in governance + * - consensus bech32 (`regenvalcons1…`) — signs blocks + * + * The agent cannot reliably match a validator against its slashing + * signing info or its governance votes without these conversions — + * that mismatch was the bug behind AGENT-004's uptime always showing + * 100% and governance participation always showing 0. + */ + +const HRP_OPERATOR = "regenvaloper"; +const HRP_ACCOUNT = "regen"; +const HRP_CONS = "regenvalcons"; + +/** Re-encode a bech32 address under a different human-readable prefix. */ +function reencodeBech32(addr: string, newHrp: string): string | null { + try { + const decoded = bech32.decode(addr); + return bech32.encode(newHrp, decoded.words); + } catch { + return null; + } +} + +/** Convert `regenvaloper1…` to the underlying `regen1…` delegator bech32. */ +export function operatorToDelegator(operatorAddress: string): string | null { + return reencodeBech32(operatorAddress, HRP_ACCOUNT); +} + +/** Convert `regen1…` delegator bech32 back to the operator form. */ +export function delegatorToOperator(delegatorAddress: string): string | null { + return reencodeBech32(delegatorAddress, HRP_OPERATOR); +} + +/** + * Derive the `regenvalcons1…` consensus address from a validator's + * ed25519 consensus public key. The pubkey bytes are SHA256-hashed + * and the first 20 bytes of the digest are bech32-encoded under the + * consensus HRP. This matches the standard Cosmos derivation in + * `tmcrypto.PubKey.Address()`. + * + * `pubkeyBase64` is the base64 string that Cosmos LCDs expose under + * `validator.consensus_pubkey.key`. + */ +export function consensusPubkeyToConsAddress( + pubkeyBase64: string +): string | null { + if (!pubkeyBase64) return null; + let pubkeyBytes: Buffer; + try { + pubkeyBytes = Buffer.from(pubkeyBase64, "base64"); + } catch { + return null; + } + if (pubkeyBytes.length === 0) return null; + + const digest = createHash("sha256").update(pubkeyBytes).digest(); + const addressBytes = digest.subarray(0, 20); + try { + return bech32.encode(HRP_CONS, bech32.toWords(addressBytes)); + } catch { + return null; + } +} diff --git a/agent-004-validator-monitor/src/index.ts b/agent-004-validator-monitor/src/index.ts index 02aef72..6c95317 100644 --- a/agent-004-validator-monitor/src/index.ts +++ b/agent-004-validator-monitor/src/index.ts @@ -74,17 +74,30 @@ async function main() { if (runOnce) { await runCycle(ledger); } else { - await runCycle(ledger); + // Recursive setTimeout rather than setInterval so a slow cycle + // never overlaps with the next tick. If a cycle takes longer + // than pollIntervalMs the next tick simply starts late — we + // never have two runCycle invocations sharing the SQLite + // connection in parallel. + let timeoutId: NodeJS.Timeout | null = null; + let stopping = false; + + const runNext = () => { + runCycle(ledger) + .catch((err) => console.error(`Cycle failed:`, err)) + .finally(() => { + if (!stopping) { + timeoutId = setTimeout(runNext, config.pollIntervalMs); + } + }); + }; - const interval = setInterval(() => { - runCycle(ledger).catch((err) => - console.error(`Cycle failed:`, err) - ); - }, config.pollIntervalMs); + runNext(); const shutdown = () => { console.log("\nShutting down gracefully..."); - clearInterval(interval); + stopping = true; + if (timeoutId !== null) clearTimeout(timeoutId); store.close(); process.exit(0); }; diff --git a/agent-004-validator-monitor/src/store.ts b/agent-004-validator-monitor/src/store.ts index 5608562..ef6ad65 100644 --- a/agent-004-validator-monitor/src/store.ts +++ b/agent-004-validator-monitor/src/store.ts @@ -12,11 +12,17 @@ const DB_PATH = path.join(__dirname, "..", "agent-004.db"); * WF-VM-02 until a real tx-stream lands), scorecards, decentralization * snapshots, and workflow execution history. */ -class Store { +export class Store { private db: Database.Database; - constructor() { - this.db = new Database(DB_PATH); + /** + * `dbPath` defaults to the per-agent DB file on disk. Tests can + * pass `":memory:"` (or a temp file) to avoid clobbering the shared + * DB file and to run test suites in parallel without hitting the + * `database is locked` failure mode. + */ + constructor(dbPath: string = DB_PATH) { + this.db = new Database(dbPath); this.db.pragma("journal_mode = WAL"); this.migrate(); } @@ -154,16 +160,38 @@ class Store { operatorAddress: string, sinceIso: string ): number { - // Every row after the first recorded rate represents a change. - // Subtract 1 to discount the baseline row (if any) that predates - // the window. - const row = this.db + // Count every distinct commission-rate row recorded in the + // trailing window. The trick is the "is the first row in the + // window actually a change" question: if there's an older row + // from *before* the window, the first in-window row *is* a change + // (the rate transitioned from the older one). If there is no + // older row, the first in-window row is the baseline read and + // should not be counted. + // + // The previous implementation counted `cnt - 1`, which dropped a + // real commission change whenever the baseline read fell outside + // the window — e.g. "validator changed commission once in the + // last 30 days but last recorded 40 days ago" returned 0. + const inWindow = this.db .prepare( `SELECT COUNT(*) AS cnt FROM commission_history WHERE operator_address = ? AND captured_at >= ?` ) .get(operatorAddress, sinceIso) as { cnt: number }; - return Math.max(0, row.cnt - 1); + + if (inWindow.cnt === 0) return 0; + + const hasBaseline = this.db + .prepare( + `SELECT 1 FROM commission_history + WHERE operator_address = ? AND captured_at < ? LIMIT 1` + ) + .get(operatorAddress, sinceIso) as { 1: number } | undefined; + + // When a baseline row exists outside the window, every in-window + // row is a change. Otherwise the first in-window row is the + // baseline and the remaining rows are changes. + return hasBaseline ? inWindow.cnt : Math.max(0, inWindow.cnt - 1); } // ── Scorecards ───────────────────────────────────────────── @@ -239,10 +267,17 @@ class Store { snapshot: string; captured_at: string; } | null { + // `LIMIT 1 OFFSET 0` intentionally — the caller invokes this in + // the `orient` phase before the current cycle's snapshot has + // been written, so the newest row in the table is the actual + // "previous" cycle's snapshot. An `OFFSET 1` here would skip + // that row and compare against the cycle before last, which + // shifts the trend analysis by one full cycle and returns null + // on the agent's second run (when only one row exists). const row = this.db .prepare( `SELECT nakamoto, gini, top10_pct, health, snapshot, captured_at - FROM decentralization_snapshots ORDER BY id DESC LIMIT 1 OFFSET 1` + FROM decentralization_snapshots ORDER BY id DESC LIMIT 1` ) .get() as | { diff --git a/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts index 6c42376..157e4f5 100644 --- a/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts +++ b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts @@ -130,7 +130,13 @@ export function createDecentralizationMonitorWorkflow( const total = tokensBig.reduce((acc, t) => acc + t, 0n); const sortedDesc = [...tokensBig].sort((a, b) => (a > b ? -1 : a < b ? 1 : 0)); - const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0") / 1_000_000n)); + // Convert uregen → JS number without pre-dividing: integer + // division by 1_000_000n floors validators with less than 1 + // REGEN to zero and loses every fractional REGEN for larger + // stakes, which is load-bearing for Gini. Number.MAX_SAFE_INTEGER + // is 2^53-1 ≈ 9e15 uregen ≈ 9 trillion REGEN, so the whole + // uregen value fits safely — no pre-scaling needed. + const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0"))); const sortedAscNum = [...tokensNum].sort((a, b) => a - b); const nakamoto = nakamotoCoefficient(sortedDesc, total); diff --git a/agent-004-validator-monitor/src/workflows/performance-tracking.ts b/agent-004-validator-monitor/src/workflows/performance-tracking.ts index 192cb84..b191c2d 100644 --- a/agent-004-validator-monitor/src/workflows/performance-tracking.ts +++ b/agent-004-validator-monitor/src/workflows/performance-tracking.ts @@ -3,6 +3,10 @@ import { store } from "../store.js"; import { config } from "../config.js"; import { describePerformanceReport } from "../monitor.js"; import { output } from "../output.js"; +import { + operatorToDelegator, + consensusPubkeyToConsAddress, +} from "../bech32.js"; import type { OODAWorkflow } from "../ooda.js"; import type { Validator, @@ -61,14 +65,6 @@ interface Actions { alertsSent: number; } -function operatorToAccountBech32(_operatorAddress: string): string { - // Proper conversion from operator (regenvaloper1…) to delegator - // bech32 (regen1…) requires pulling out the underlying bytes and - // re-encoding with the delegator HRP. The MVP falls back to matching - // by moniker / position inside getVoteForVoter, so we return an - // empty string here and let the governance fetch short-circuit. - return ""; -} export function createPerformanceTrackingWorkflow( ledger: LedgerClient @@ -125,6 +121,13 @@ export function createPerformanceTrackingWorkflow( const scorecards: ValidatorScorecard[] = []; const alerts: PerformanceAlert[] = []; + // Compute the trailing-window cutoff once — it does not depend + // on the validator, so there is no point recomputing it per + // iteration the way the original loop did. + const sinceIso = new Date( + Date.now() - config.validator.uptimeTrailingDays * 86_400_000 + ).toISOString(); + // Record a commission baseline for every validator we see this // cycle; the commission history drives the stability penalty. for (const v of validators) { @@ -140,12 +143,20 @@ export function createPerformanceTrackingWorkflow( bonded > 0n ? Number((tokens * 10000n) / bonded) / 100 : 0; // ── Uptime component ───────────────────────────────── - // When we can't find signing info, assume 100% (signed every - // window) rather than penalizing — better to under-count real - // issues than to smear a healthy validator. - const signing = signingByConsAddrLike.get( - v.consensus_pubkey?.key || "" - ); + // Derive the regenvalcons1… address from the validator's + // consensus pubkey so it matches the keys in + // signingByConsAddrLike (which are `info.address`, i.e. the + // valcons bech32). Previously we were joining a base64 + // pubkey string against a bech32 address and the lookup + // always missed — every validator was reporting 0 missed + // blocks, which masked real downtime. When the derivation or + // the lookup still fails we fall back to 100% uptime rather + // than penalizing a validator we cannot classify. + const pubkeyBase64 = v.consensus_pubkey?.key || ""; + const consAddr = consensusPubkeyToConsAddress(pubkeyBase64); + const signing = consAddr + ? signingByConsAddrLike.get(consAddr) + : undefined; const missedBlocks = signing ? Number(signing.missed_blocks_counter || "0") : 0; @@ -159,13 +170,13 @@ export function createPerformanceTrackingWorkflow( ); // ── Governance component ───────────────────────────── - // MVP: count finalized proposals as the denominator and give - // every validator credit for 0 votes (since the operator→account - // conversion is future work). This intentionally keeps the - // governance score at 0 across the set so no validator is - // punished relative to another. A follow-up PR will plug in - // real vote records per-validator. - void operatorToAccountBech32(v.operator_address); + // Derive the delegator bech32 from the operator address so we + // could plug in real per-validator vote records downstream. + // The observe phase still leaves `votesCastByOperator` empty + // in this PR (governance scoring is batched in WF-VM-03), so + // every validator is treated as 0-votes relative to the same + // denominator until that wiring lands. + void operatorToDelegator(v.operator_address); const proposalsConsidered = obs.finalizedProposals.length; const votesCast = obs.votesCastByOperator.get(v.operator_address) ?? 0; const governanceParticipation = @@ -181,9 +192,6 @@ export function createPerformanceTrackingWorkflow( // const` in config.ts otherwise infers a literal type. let stabilityScore: number = config.validator.scoreWeightStability; if (v.jailed) stabilityScore -= config.validator.stabilityPenaltyJailing; - const sinceIso = new Date( - Date.now() - config.validator.uptimeTrailingDays * 86_400_000 - ).toISOString(); const commissionChanges = store.countCommissionChangesSince( v.operator_address, sinceIso From 12246ebb3fc25b102d52ff14e3289b16b59bff5e Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sat, 11 Apr 2026 15:00:39 -0700 Subject: [PATCH 5/5] fix(agent-004): address Gemini review on real delegation tx-stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini review feedback on PR #104: LedgerClient.parseDelegationEventsFromTx: * Walk `tx.logs` message-by-message so each staking event is attributed to the delegator (`sender`) of its own message. The previous implementation flattened all events, collected every `message.sender` into a positional array, and mapped the Nth staking event to the Nth sender. That mis-attributed staking events whenever a tx mixed staking with non-staking messages (e.g. `[MsgDelegate, MsgSend, MsgUndelegate]` would attribute the undelegate to the MsgSend sender). * Prefer `tx.logs[i].events[]` over a mixed `tx.logs + tx.events` merge. In modern Cosmos SDK versions `tx.events` is a flattened superset of everything in the logs, so walking both double-counts every staking event (delegation deltas ended up 2× reality). We only fall back to `tx.events` when `tx.logs` is empty (very old LCD builds). LedgerClient.getRecentDelegationTxs: * Dedupe transactions by hash across the three per-type-URL queries so a tx containing more than one of the three message types (e.g. atomic `MsgDelegate` + `MsgUndelegate`) is only parsed once. Previously every event in such a tx was aggregated multiple times, inflating the per-cycle delegation deltas. * Replace the silent `catch {}` with `console.error` so network failures are visible. Also cherry-picks the PR #81 Gemini-review fixes so this branch is self-consistent (bech32 consensus address derivation, Gini precision, OFFSET 1 → 0, commission baseline, recursive setTimeout main loop). Tests: updated the "multiple staking events in a single batched tx" case to use the realistic one-log-per-message shape, and added a new "attributes staking events through interleaved non-staking messages" case that directly pins the bug the previous positional-match code was broken by. Full vitest suite: 56/56 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/ledger.test.ts | 72 ++++++- agent-004-validator-monitor/src/ledger.ts | 186 +++++++++++------- 2 files changed, 177 insertions(+), 81 deletions(-) diff --git a/agent-004-validator-monitor/src/ledger.test.ts b/agent-004-validator-monitor/src/ledger.test.ts index 4c7a594..ee93311 100644 --- a/agent-004-validator-monitor/src/ledger.test.ts +++ b/agent-004-validator-monitor/src/ledger.test.ts @@ -117,9 +117,13 @@ describe("LedgerClient.parseDelegationEventsFromTx", () => { }); it("parses multiple staking events in a single batched tx", () => { - // Two delegations and one undelegation in one tx, with positional - // sender matching — sender[0] → first staking event, sender[1] → - // second, sender[2] → third. + // A Cosmos SDK tx with three messages surfaces them as three + // separate `logs[]` entries, each carrying its own message event + // with its own sender. We iterate log-by-log so each staking + // event is attributed to its own delegator rather than + // positionally mapping against a flattened sender array (which + // would mis-attribute if a tx mixed staking with non-staking + // messages). const tx = { txhash: "tx-batched", timestamp: "2026-02-18T12:00:00Z", @@ -127,8 +131,6 @@ describe("LedgerClient.parseDelegationEventsFromTx", () => { { events: [ { type: "message", attributes: [{ key: "sender", value: "regen1d1" }] }, - { type: "message", attributes: [{ key: "sender", value: "regen1d2" }] }, - { type: "message", attributes: [{ key: "sender", value: "regen1d3" }] }, { type: "delegate", attributes: [ @@ -136,6 +138,11 @@ describe("LedgerClient.parseDelegationEventsFromTx", () => { { key: "amount", value: "100uregen" }, ], }, + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d2" }] }, { type: "delegate", attributes: [ @@ -143,6 +150,11 @@ describe("LedgerClient.parseDelegationEventsFromTx", () => { { key: "amount", value: "200uregen" }, ], }, + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d3" }] }, { type: "unbond", attributes: [ @@ -161,6 +173,56 @@ describe("LedgerClient.parseDelegationEventsFromTx", () => { expect(out[2]!.delegator).toBe("regen1d3"); }); + it("attributes staking events through interleaved non-staking messages", () => { + // The old positional-match code would have attributed the + // undelegate to the MsgSend sender here, because the senders + // array would have been [d1, sender-of-send, d3] and the third + // staking event (there's only one) would fall through to the + // wrong slot. Iterating log-by-log fixes that. + const tx = { + txhash: "tx-interleaved", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1delegator" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1a" }, + { key: "amount", value: "100uregen" }, + ], + }, + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1sendsomething" }] }, + // No staking event in this message. + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1undelegator" }] }, + { + type: "unbond", + attributes: [ + { key: "validator", value: "regenvaloper1a" }, + { key: "amount", value: "50uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(2); + expect(out[0]!.eventType).toBe("delegate"); + expect(out[0]!.delegator).toBe("regen1delegator"); + expect(out[1]!.eventType).toBe("undelegate"); + expect(out[1]!.delegator).toBe("regen1undelegator"); + }); + it("ignores delegate events with missing validator attribute", () => { const tx = { txhash: "tx-missing-validator", diff --git a/agent-004-validator-monitor/src/ledger.ts b/agent-004-validator-monitor/src/ledger.ts index 612c8a6..08f01b9 100644 --- a/agent-004-validator-monitor/src/ledger.ts +++ b/agent-004-validator-monitor/src/ledger.ts @@ -152,6 +152,13 @@ export class LedgerClient { "/cosmos.staking.v1beta1.MsgBeginRedelegate", ]; + // Dedup transactions by hash so a tx containing more than one of + // the three message types (e.g. an atomic `MsgDelegate` + + // `MsgUndelegate` in the same tx) is only parsed once, no matter + // how many of the per-type queries return it. Without this dedup + // every event in that tx would get aggregated multiple times, + // inflating the per-cycle delegation deltas. + const seenHashes = new Set(); const results: DelegationEvent[] = []; for (const typeUrl of typeUrls) { @@ -166,12 +173,19 @@ export class LedgerClient { const txResponses = (data.tx_responses || []) as Array>; for (const tx of txResponses) { + const hash = typeof tx.txhash === "string" ? tx.txhash : ""; + if (hash && seenHashes.has(hash)) continue; + if (hash) seenHashes.add(hash); results.push(...this.parseDelegationEventsFromTx(tx)); } - } catch { + } catch (err) { // Per-type-url failure is isolated — the other two still run. - // An off-chain aggregator that loses one of three queries is - // still better than falling back to the token-delta proxy. + // Log rather than swallow so transient LCD issues are visible + // instead of silently falling back to empty delegation data. + console.error( + `LedgerClient.getRecentDelegationTxs(${typeUrl}) failed:`, + err + ); } } @@ -187,90 +201,110 @@ export class LedgerClient { * - `unbond` (MsgUndelegate) * - `redelegate` (MsgBeginRedelegate) * - * Also reads the `message` event to extract the delegator address - * (sender) because the staking events themselves don't carry it. - * The association is positional: the Nth staking event in a tx - * maps to the Nth message event's sender. + * Walks `tx.logs` message-by-message so each staking event is + * attributed to the delegator (`sender`) of its own message. A + * positional mapping against a flattened senders array would + * mis-attribute staking events whenever a tx mixes staking with + * non-staking messages — for example, + * `[MsgDelegate, MsgSend, MsgUndelegate]` would attribute the + * undelegate to the MsgSend sender. + * + * Falls back to `tx.events` only when `tx.logs` is empty (old LCD + * builds). Even in the fallback case we preserve the "scope senders + * to a single message" semantics by treating the flat list as one + * scope, which is an imperfect match but strictly no worse than + * the walk we replaced. */ parseDelegationEventsFromTx(tx: Record): DelegationEvent[] { const txHash = typeof tx.txhash === "string" ? tx.txhash : ""; const timestamp = typeof tx.timestamp === "string" ? tx.timestamp : new Date().toISOString(); - const collected: Array> = []; const logs = (tx.logs || []) as Array>; - for (const log of logs) { - collected.push(...((log.events || []) as Array>)); - } - collected.push(...((tx.events || []) as Array>)); - - // Sender addresses from message events, in order of appearance. - const senders: string[] = []; - for (const ev of collected) { - if (typeof ev.type === "string" && ev.type === "message") { - const attributes = (ev.attributes || []) as Array<{ key: string; value: string }>; - const sender = attributes.find((a) => a.key === "sender"); - if (sender) senders.push(sender.value); + const eventScopes: Array>> = []; + if (logs.length > 0) { + for (const log of logs) { + eventScopes.push( + (log.events || []) as Array> + ); } + } else { + // No per-message logs — very old LCD build. Fall back to the + // flattened `tx.events` list as a single scope. + eventScopes.push( + (tx.events || []) as Array> + ); } const results: DelegationEvent[] = []; - let senderIdx = 0; - - const nextSender = (): string => { - const s = senders[senderIdx] ?? ""; - senderIdx++; - return s; - }; - - for (const ev of collected) { - const type = typeof ev.type === "string" ? ev.type : ""; - const attributes = (ev.attributes || []) as Array<{ key: string; value: string }>; - const attr = (k: string): string | null => { - const hit = attributes.find((a) => a.key === k); - return hit ? hit.value : null; - }; - - if (type === "delegate") { - const validator = attr("validator") ?? ""; - const amount = parseCoinAmount(attr("amount")); - if (!validator || amount === null) continue; - results.push({ - txHash, - eventType: "delegate", - delegator: nextSender(), - validator, - sourceValidator: null, - amountUregen: amount, - occurredAt: timestamp, - }); - } else if (type === "unbond") { - const validator = attr("validator") ?? ""; - const amount = parseCoinAmount(attr("amount")); - if (!validator || amount === null) continue; - results.push({ - txHash, - eventType: "undelegate", - delegator: nextSender(), - validator, - sourceValidator: null, - amountUregen: amount, - occurredAt: timestamp, - }); - } else if (type === "redelegate") { - const src = attr("source_validator") ?? ""; - const dst = attr("destination_validator") ?? ""; - const amount = parseCoinAmount(attr("amount")); - if (!src || !dst || amount === null) continue; - results.push({ - txHash, - eventType: "redelegate", - delegator: nextSender(), - validator: dst, - sourceValidator: src, - amountUregen: amount, - occurredAt: timestamp, - }); + + for (const events of eventScopes) { + // Within one message's events there is at most one relevant + // `message.sender` attribute — capture it once and use it for + // every staking event in the same scope. + let sender = ""; + for (const ev of events) { + if (typeof ev.type === "string" && ev.type === "message") { + const attributes = + (ev.attributes || []) as Array<{ key: string; value: string }>; + const hit = attributes.find((a) => a.key === "sender"); + if (hit) { + sender = hit.value; + break; + } + } + } + + for (const ev of events) { + const type = typeof ev.type === "string" ? ev.type : ""; + const attributes = + (ev.attributes || []) as Array<{ key: string; value: string }>; + const attr = (k: string): string | null => { + const hit = attributes.find((a) => a.key === k); + return hit ? hit.value : null; + }; + + if (type === "delegate") { + const validator = attr("validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!validator || amount === null) continue; + results.push({ + txHash, + eventType: "delegate", + delegator: sender, + validator, + sourceValidator: null, + amountUregen: amount, + occurredAt: timestamp, + }); + } else if (type === "unbond") { + const validator = attr("validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!validator || amount === null) continue; + results.push({ + txHash, + eventType: "undelegate", + delegator: sender, + validator, + sourceValidator: null, + amountUregen: amount, + occurredAt: timestamp, + }); + } else if (type === "redelegate") { + const src = attr("source_validator") ?? ""; + const dst = attr("destination_validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!src || !dst || amount === null) continue; + results.push({ + txHash, + eventType: "redelegate", + delegator: sender, + validator: dst, + sourceValidator: src, + amountUregen: amount, + occurredAt: timestamp, + }); + } } }