diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..5cd0c5c --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1 @@ +{"$schema":"https://unpkg.com/@changesets/config@3.1.1/schema.json","changelog":"@changesets/cli/changelog","commit":false,"fixed":[],"linked":[],"access":"public","baseBranch":"main","updateInternalDependencies":"patch","ignore":[]} diff --git a/.changeset/first-release.md b/.changeset/first-release.md new file mode 100644 index 0000000..a27363b --- /dev/null +++ b/.changeset/first-release.md @@ -0,0 +1,6 @@ +--- +"@bloxwap/agent-cli": minor +--- + +Add managed Hyperliquid agents, Relay funding, JSON commands, a persistent runner, +risk checks, durable events/audit journals, and the packaged trading skill. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6d10f1f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI +on: + push: + branches: [main] + pull_request: +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run lint + - run: npm run typecheck + - run: npm test diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..69a3401 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,23 @@ +name: Documentation +on: + workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write +jobs: + docs: + runs-on: ubuntu-latest + environment: github-pages + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run docs:generate + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..e3ca238 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,31 @@ +name: Testnet lifecycle +on: + schedule: + - cron: '23 4 * * *' + workflow_dispatch: +permissions: + contents: read +jobs: + testnet: + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: testnet + env: + BLOXWAP_E2E: '1' + BLOXWAP_TEST_MASTER_KEY: ${{ secrets.BLOXWAP_TEST_MASTER_KEY }} + BLOXWAP_PRIVY_APP_ID: ${{ secrets.BLOXWAP_PRIVY_APP_ID }} + BLOXWAP_PRIVY_APP_SECRET: ${{ secrets.BLOXWAP_PRIVY_APP_SECRET }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run test:e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: testnet-journals + path: artifacts/ diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 0000000..30b844e --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,23 @@ +name: Install smoke +on: + pull_request: + push: + branches: [main] +jobs: + install: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: oven-sh/setup-bun@v2 + - run: npm ci + - run: npm run build + - run: npm run test:package + env: + BLOXWAP_SMOKE_BUN: '1' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..41f482b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release +on: + push: + tags: ['v*'] + workflow_dispatch: +permissions: + contents: write + id-token: write +jobs: + release: + if: vars.BLOXWAP_PUBLISH == '1' + runs-on: ubuntu-latest + environment: npm-release + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + - uses: oven-sh/setup-bun@v2 + - run: npm ci + - name: Verify tag and package version + if: startsWith(github.ref, 'refs/tags/') + run: node -e 'const p=require("./package.json");if(process.env.GITHUB_REF_NAME!==`v${p.version}`)throw Error("Tag/version mismatch")' + - run: npm run build + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run test:package + - run: npm run build:binaries + - run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + generate_release_notes: true + files: | + binaries/* + SKILL.md + CHANGELOG.md diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml new file mode 100644 index 0000000..389fe60 --- /dev/null +++ b/.github/workflows/version.yml @@ -0,0 +1,21 @@ +name: Version packages +on: + workflow_dispatch: +permissions: + contents: write + pull-requests: write +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - uses: changesets/action@v1 + with: + version: npm run version:packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e50b39c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +coverage/ +.env +*.tgz +site/ +artifacts/ +.bloxwap/ +binaries/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2ef7ba2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +site/ +binaries/ +src/core/skill-content.ts +docs/issues/ +docs/commands.md +test/__snapshots__/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..fe00293 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{"singleQuote":true,"trailingComma":"all"} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8c8e460 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# @bloxwap/agent-cli + +## 0.1.0 + +Initial implementation of managed-agent trading, funding and persistent runner. +Use Changesets for subsequent versioning and release notes. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2aa03ea --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# agent-cli + +An agent-native Hyperliquid trading CLI with JSON input/output, a persistent +signing runner, Privy managed agents, Relay funding, and a packaged agent skill. + +Install: `npm i -g @bloxwap/agent-cli` or `bunx @bloxwap/agent-cli --help`. +The requested Hyperliquid SDK declares Node **22.12+**; use that supported runtime +or current Bun. No postinstall script. Publishing is release-gated. + +## Testnet quickstart + +```sh +npm ci +npm run build +node dist/cli.js --help +node dist/cli.js config set privyAppId YOUR_APP_ID +node dist/cli.js config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET +# Set BLOXWAP_PRIVY_APP_SECRET securely outside the CLI/conversation. +node dist/cli.js init --network testnet --master-address 0xYOUR_MASTER_ADDRESS +# Sign returned EIP-712 approval externally, then submit its 65-byte signature: +node dist/cli.js init --network testnet --signature 0xYOUR_SIGNATURE +node dist/cli.js fund --network testnet --amount 10 --origin-chain 42161 +# Show QR/address and exact origin token/fees to user; complete any deposit steps. +node dist/cli.js status --network testnet +# Continue only when active deposits report settled. +node dist/cli.js run --network testnet --detach +node dist/cli.js market --coin BTC +node dist/cli.js order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc"}' +node dist/cli.js events --follow +``` + +Testnet onboarding/trades require Privy sandbox credentials, an externally signed +testnet master approval and testnet collateral. Relay HyperCore testnet routes may +be unavailable; the e2e suite mocks Relay. The placeholders above must be replaced +with addresses/signatures from your test environment. Never paste a private key. + +## Architecture and custody + +```mermaid +flowchart LR + Agent[Agent / CLI] -->|private JSON-RPC| Runner[Persistent runner] + Runner -->|P-256 authorization| Privy[Privy managed agent] + Privy -->|EIP-712 signature| HL[Hyperliquid exchange] + Agent --> Relay[Relay funding quote] + Master[External master wallet] -->|one-time approvals| HL + Runner --> Journal[Events / audit / logs] +``` + +Master custody stays external. Privy owns the separate agent key; the CLI holds +only an authorization key. Hyperliquid agent permissions deny withdrawal. +Privy policies restrict signing domains; local checks enforce configured risk +limits. Read [custody limits](docs/custody.md) before using a funded account. + +State: `~/.bloxwap`; config: `~/.bloxwap/config.json`; IPC: +`~/.bloxwap/run.sock`; env prefix: `BLOXWAP_`. All local state is private to the OS +user. The default Bloxwap builder address is +`0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (0.01%). +Override via `config set builder.address` / `config set builder.fee` or +`BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`; init requests builder approval. + +## Development and docs + +`npm run build`, `npm run lint`, `npm run typecheck`, `npm test` must pass before +committing. Unit tests mock services. Credential-dependent tests are gated: `npm run test:sandbox` and `npm run test:e2e` skip without the required environment. The master-key testnet harness is CI-only; unit tests never access the network. + +`npm run docs:generate` creates [command schemas](docs/commands.md) and a static +site in `site/`. [Docs index](docs/index.md), [security](docs/security.md), +[error catalog](docs/errors.md), and [SKILL.md](SKILL.md) explain operation. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..86bbd11 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,121 @@ +--- +name: agent-cli +description: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery. +metadata: + version: "0.1.0" +--- + +Use `agent-cli` for the user's authorized Hyperliquid account operations. Obtain +command schemas with `agent-cli schema COMMAND`; arguments can be flags or +`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one +JSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`. +Logs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines. +Use `--network testnet` during testing; never silently change to mainnet. + +## Onboarding sequence + +1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP` + and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets + the environment secret outside the conversation. The default Bloxwap builder is + `0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (1 bp = 0.01%). + Override with `config set builder.address` / `config set builder.fee` or + `BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`. +2. Run `init --master-address 0x... --network testnet`. Show the returned approval + payload/deeplink/QR. The user signs in their master wallet; submit only the + resulting signature with `init --signature 0x...`. Submit builder approval + separately with `init --builder-signature 0x...` for the configured 0.01% default. Continue only after `approved:true`. +3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address, + origin chain/token, minimum, fees, expected received amount and expiry. On + testnet, do not treat the default Arbitrum mainnet chain/token as a test route: + use an explicitly supported test chain/token and `originDecimals`, or the + credential-gated CI harness with mocked Relay and pre-funded test collateral. + If no test route is known, stop funding and report the missing route. The user + transfers the quoted origin token. If `signatureSteps` require signatures, + show the exact payload and submit each via `deposit` in returned order. +4. Poll `status` until each active deposit is `settled`, for at most 12 polls + or 5 minutes, whichever comes first. Start with 1-second delays, double to a + 15-second cap, and stop with an explicit pending report if the budget expires. + Settlement requires Relay success and Hyperliquid balance receipt. Stop on + failed/refunded deposits and explain their status. Do not place orders while + any deposit is awaiting funds or pending. +5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the + last handled sequence; replay after reconnect. An unfilled or rejected order + remains incomplete: do not claim the requested trade succeeded. A fill updates the trading + plan, an error follows the catalog below, and expiry warnings prompt rotation + using external master-wallet approval. + +## Commands and example envelopes + +Examples show representative `data`, not live balances or service promises. +Full inputs/outputs are discoverable with `schema`; amounts/prices are strings. + +| Input | Example output | +| --- | --- | +| `--help` | `{"ok":true,"data":{"commands":[{"name":"status","description":"Read account status"}],"globalFlags":["--json","--config","--network","--pretty"]}}` | +| `schema order` | `{"ok":true,"data":{"input":{"type":"object"},"output":{"oneOf":[]}}}` (actual schemas include properties) | +| `config get network` | `{"ok":true,"data":"testnet"}` | +| `config set limits.maxLeverage 5` | `{"ok":true,"data":{"version":1,"limits":{"maxLeverage":5}}}` | +| `config validate` | `{"ok":true,"data":{"valid":true,"version":1}}` | +| `init --master-address 0x...` | `{"ok":true,"data":{"approved":false,"requiresSignature":true,"approvals":{"agent":{"typedData":{}}},"qrAscii":"..."}}` | +| `fund --amount 10 --no-qr` | `{"ok":true,"data":{"depositId":"...","depositAddress":"0x...","expectedReceived":"100000000","builderFeeRate":"0.01%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","state":"awaiting_funds","qrAscii":null}}` | +| `deposit --deposit-id ID` | `{"ok":true,"data":{"depositId":"ID","steps":[{"id":"step:0","requiresSignature":true,"payload":{}}]}}` | +| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{"ok":true,"data":{"depositId":"ID","steps":[]}}` | +| `status` | `{"ok":true,"data":{"balance":{"accountValue":"100"},"agent":{"approved":true,"daysRemaining":"179"},"deposits":[],"policy":{"id":"...","version":"1"},"runner":{"pid":"123"}}}` | +| `balance` | `{"ok":true,"data":{"accountValue":"100","accountValueUnits":"10000000000","withdrawable":"96","marginUsed":"4"}}` | +| `market --coin BTC` | `{"ok":true,"data":{"coin":"BTC","asset":"0","szDecimals":"5","maxLeverage":"40","mid":"20000"}}` | +| `positions` | `{"ok":true,"data":[{"coin":"BTC","size":"0.001","entryPrice":"20000","leverage":"5","marginMode":"cross"}]}` | +| `orders` | `{"ok":true,"data":[{"coin":"BTC","oid":"123","sz":"0.001","limitPx":"20000","side":"B"}]}` | +| `order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0.01%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","cloid":"0x..."}}` | +| `order --json '{"coin":"BTC","side":"sell","size":"0.001","type":"market","slippage":0.01,"reduceOnly":true}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"filled":{"oid":123}}]}}}}}` | +| `cancel --oid 123` or `cancel --all` | `{"ok":true,"data":{"cancelled":["123"],"result":{"status":"ok"}}}` | +| `modify --json '{"modifications":[{"oid":123,"price":"20100","size":"0.001"}]}'` | `{"ok":true,"data":{"status":"ok"}}` | +| `leverage --coin BTC --leverage 5 --mode isolated` | `{"ok":true,"data":{"status":"ok"}}` | +| `run --detach` | `{"ok":true,"data":{"pid":123,"startedAt":1700000000000,"network":"testnet"}}` | +| `events --since 12 --types '["fill"]'` | `{"ok":true,"data":[{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}]}` | +| `events --follow` | `{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}` (one line per event) | +| `logs --follow --level warn` | `{"level":"warn","requestId":"...","message":"Risk rejected","time":"..."}` (JSON lines) | +| `rotate` then `rotate --signature 0x...` | `{"ok":true,"data":{"oldAgent":"0x...","newAgent":"0x...","swapped":true,"oldRevoked":false,"revokeApproval":{}}}` | +| `rotate --revoke-signature 0x...` | `{"ok":true,"data":{"swapped":true,"oldRevoked":true}}` | +| `revoke --cancel-orders` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":false,"requiresSignature":true,"approval":{}}}` | +| `revoke --signature 0x...` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":true}}` | +| `audit --intent ID` | `{"ok":true,"data":[{"intentId":"ID","stage":"decision","data":{"allowed":true},"previousHash":"...","hash":"..."}]}` | +| `skill install --target codex` | `{"ok":true,"data":{"target":"codex","path":".../agent-cli/SKILL.md","version":"0.1.0"}}` | + +Before an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and +market leverage ceiling. Compute size/notional with decimal strings/bigints, +using the user-approved limit or the bounded IOC execution price. A valid schema +alone does not establish that an order is tradable or within the budget. + +Use `--intent-id ID --intent "reason"` on trading actions to connect the user's +reasoning to the audit. Keep cloid stable when retrying a known order intent. +For TP/SL use `type:"tp"|"sl"` and `triggerPrice` with size/price/reduceOnly; +market orders use an explicit slippage-bounded IOC, not an unbounded market order. + +## Error recovery + +| Code | Action | +| --- | --- | +| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results | +| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent | +| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure | +| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback | +| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding | +| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy | +| AGENT_EXPIRED | Rotate with master-wallet approval | +| AGENT_NOT_APPROVED | Complete init/approval or explain revocation | +| INVALID_SIZE, INVALID_PRICE | Correct input using market precision | +| UNKNOWN_MARKET | Refresh metadata; use an allowed market | +| ORDER_NOT_FOUND | Refresh orders before changing intent | +| CONFIG_INVALID | Correct configuration/input; never insert secrets | +| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure | + +## Gotchas + +HyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and +bigints. Some deposits require **two distinct signatures**; resume stored steps +rather than restarting the route. Agent expiry is at most **180 days**. Never +ask for, paste, log or import a master private key or seed. The managed agent is +trade-only; local limits include markets, size/notional, leverage, daily loss and +open orders. Privy sees an L1 action hash, so it cannot independently inspect +notional/action contents; Hyperliquid agent permissions and local checks enforce +those boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`. diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md new file mode 100644 index 0000000..bb68cef --- /dev/null +++ b/docs/ACCEPTANCE.md @@ -0,0 +1,26 @@ +# Acceptance review + +This records implementation evidence, not a claim that credential-dependent +services or hosted workflows ran. See PROGRESS.md for the remaining partial +items. The final local run uses the SDK-supported Node 22 runtime and Bun. + +| Issues | Reviewed artifacts and meaningful validation | +| --- | --- | +| #1 | package scripts/bin, strict ESM tsconfig, CI workflow; build/lint/typecheck/Vitest smoke pass. Hosted CI awaits a push, which was explicitly excluded. | +| #2 | docs/adr/0001-runner-interface.md compares all three transports, method names, private permissions and Windows strategy. | +| #3–#4 | Stable append-only error catalog and serialization/exit tests; strict flags/JSON/stdin parser, input/output schemas, typo and uncaught-error tests. Streaming exceptions are explicit. | +| #5–#6 | Config validation/migration/atomic private writes/corrupt-file tests; recursive field/env/PEM redaction, request correlation and size/day rotation. | +| #7–#9 | Requested SDK, network factories, shared metadata cache/refresh, exact rounding fixtures, service mapping fixtures, bounded retries and exclusive persisted nonce tests. | +| #10–#13 | P-256 managed-wallet creation/linking and typed-data adapter; versioned Privy domain/source policy and local forbidden-action tests; resumable external master approvals with persisted per-master nonces, expiry bounds and builder approval verification. Bloxwap builder defaults to f=10 (0.01%); hash-preimage policy limitations are explicit in PROGRESS/custody docs. Sandbox calls are separately gated. | +| #14–#19 | Relay quote/fee/address fixture tests, bigint round trips including signed values, terminal QR snapshot and file renderer, idempotent funding store, ordered/resumable signature tests, terminal deposit transitions and collateral receipt excluding marked PnL. | +| #20–#25 | Numeric-string account/order/market projections, approval/policy/deposit/expiry/runner status; every risk bound tested, opposing pending exposure and realized/marked daily PnL checked; SDK order fixture tests cover limit/IOC/slippage/trigger behavior, cancellation, batch modification preserving reduce-only/TIF/trigger semantics and leverage ceilings/loss. | +| #26–#28 | Private PID/socket lifecycle, stale dead-PID recovery/readiness, serialized RPC, cross-network/config refusal and direct fallback only on confirmed absence; simulated WS drop, SDK resubscribe, paginated info gap-fill, persisted fill dedupe. Windows ACL limitations are documented. | +| #29–#32 | Locked sequence journal across independent writers/restarts, replay/types/follow, exact fill fee units; log filtering/redaction; runner heartbeat/staleness and deposit polling; mocked-clock expiry blocking and once-per-threshold warnings. | +| #33–#36 | New approval verified before signer handoff/old revocation, resumable rotation and immediate local revocation tests; fsynced cross-process hash-chain with intent/decision/signature digest/response/fill linkage; tamper/redaction tests and linked full-lifecycle records. CLI/RPC pre-schema hex/mnemonic detector and threat model. | +| #37–#38 | Frontmatter validator, independent simulated skill forward-test (schema-validated actions, ordered signatures, pending/rejection/expiry handling), bounded polling and supported-test-route guidance; install/custody README, generated input/output command reference and static Pages site. Real agent/testnet validation is credential-gated. | +| #39 | Offline full CLI/SDK/RPC account lifecycle test; separate opt-in nightly testnet script signs only in CI, uses mocked Relay and funded test collateral, covers onboarding/orders/events/rotation/revoke and exports event/audit artifacts. Live execution skips without secrets. | +| #40–#42 | Packed global installation and Bun smoke, executable bin, engine declaration, no package postinstall; Linux/macOS binary build, changesets/tag/version/provenance/asset pipeline behind publish gate; packaged/embedded skill, Claude/Codex paths, CODEX_HOME, overwrite protection and matching versions. Publishing/registry verification awaits external credentials. | + +No push, tag creation, remote changes, package publication or live-account trading +was performed. Local acceptance deliberately leaves unsupported external claims +unproven instead of labelling skipped checks as successful live tests. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md new file mode 100644 index 0000000..204bd44 --- /dev/null +++ b/docs/PROGRESS.md @@ -0,0 +1,49 @@ +# Implementation progress + +| Issue | State | Notes | +| --- | --- | --- | +| #1 | done | Strict TypeScript ESM scaffold, bundled CLI, Vitest smoke test and CI checks. | + +| #2 | done | ADR selects private newline-framed JSON-RPC socket; defines methods and Windows strategy. | +| #3 | done | Stable error catalog, serialization, exit codes and append-only guard. | +| #4 | done | Validated JSON/flags/stdin router, JSON schemas, envelopes and exit codes. | +| #5 | done | Versioned validated config, atomic private writes, migration, commands and env overrides. | +| #6 | done | JSON logging, recursive redaction, correlation IDs and size/day rotation. | +| #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; final verification uses supported Node 22 and Bun. Shared metadata cache is exposed via market discovery. | +| #8 | done | Service error adapters, redacted upstream detail, fixture-driven classification and retry defaults. | +| #9 | done | Bounded jittered retries and per-signer persisted monotonic nonces guarded by exclusive locks. Stale locks fail closed. | +| #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | +| #11 | partial | Versioned domain/network Privy policy and local trade allowlist; L1 action hash prevents Privy per-action/notional inspection. Exchange agent permissions enforce withdrawal boundary. | +| #12 | done | Idempotent managed-wallet onboarding, persisted external master EIP-712 approvals/deeplink, expiry bound, agent/builder verification and master-key rejection. | +| #13 | done | Default Bloxwap builder address with f=10 (0.01%); config/env overrides, init approval, approved-maximum checks and fund/order address/rate reporting. | +| #14 | done | Typed validated Relay quote/status adapter, perps USDC destination, multiple origin routes and fee parsing; mocked fixtures. | +| #15 | done | Exact bigint decimal parse/format and loss-detecting 6↔8 conversions with property-style round trips. | +| #16 | done | Terminal QR on stderr and JSON field, optional PNG/SVG, no-qr support and snapshot. | +| #17 | done | Funding command returns address/fees/minimum/expiry/8-decimal received amount, persists deposits and reuses valid quotes. | +| #18 | done | Resumable ordered master-signature steps, trusted Relay submission endpoints and funding flow docs. | +| #19 | done | Persisted terminal deposit lifecycle, Relay + exact balance settlement, retryable trading gate and runner emission hook. | +| #20 | done | Account read commands, numeric strings, deposits/approval/policy/expiry status; runner preference wired in #27. | +| #21 | done | Exact decimal risk evaluation covers markets, size/notional/position/leverage/loss/open-order limits; trade hooks and audit wired in #22/#35. | +| #22 | done | Limit Gtc/Ioc/Alo, slippage IOC market, TP/SL, rounding, cloid, risk/deposit/expiry gates, builder and Privy signing; runner routing in #27. | +| #23 | done | Batch cancellation by oid/cloid, coin or account; unknown order normalization and mocked tests. | +| #24 | done | Single/batch modification by oid/cloid with rounding and pre-signing risk checks. | +| #25 | done | Cross/isolated leverage updates enforce market and configured ceilings with mocked tests. | +| #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Dead PID recovery and explicit readiness; stale startup locks fail closed. | +| #27 | done | Bounded newline JSON-RPC socket with private mode, method aliases, serialized trading, normalized errors and configurable direct fallback. | +| #28 | done | SDK reconnect/resubscribe + heartbeat transport, user/market subscriptions, info gap-fill and persisted tid dedupe; simulated drop tests. | +| #29 | done | Typed persisted monotonic event journal, replay/filter/follow stream, subscription/deposit emission and shutdown flush. | +| #30 | done | JSON-line log replay/follow across rotated files, minimum-level/time/request filtering and redaction. | +| #31 | done | Runner heartbeat/state events, PID/uptime/WS/signing health and stale detection; background deposit polling. | +| #32 | done | Persisted once-per-expiry threshold warnings in runner events and nonretryable expiry trading block; mocked clock tests. | +| #33 | done | Persisted two-approval rotation, verified new agent before serialized runner hot-swap, old revocation only after handoff; rotation audit hook in #35. | +| #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. | +| #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. | +| #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. | +| #37 | partial | Independently forward-tested, schema-validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | +| #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. | +| #39 | partial | Full mocked CLI/SDK/RPC lifecycle passes; opt-in real testnet lifecycle with mocked Relay, nightly secrets gate and journal artifacts. Real execution skipped without credentials. | +| #40 | partial | Postinstall-free executable package, supported Node 22.12+ engine and global/Bun install smoke pass locally; macOS/Linux CI and optional compiled binaries. Registry install awaits publication. | +| #41 | partial | Changesets/version PR workflow, gated tag publish with npm provenance, generated release notes and binary/skill assets; publishing not run without registry credentials. | +| #42 | done | Final packaged flow audited and corrected (network/config isolation, exact loss/exposure/receipt checks, trigger preservation, readiness and stream errors). Skill shipped/embedded in npm and compiled CLI; Claude/Codex installation with CODEX_HOME, version synchronization, idempotency and overwrite protection. | + +Final local verification: supported Node 22, strict build/lint/typecheck and isolated offline tests. Sandbox/testnet scripts skip without credentials. npm audit reports zero vulnerabilities. Bloxwap builder defaults to f=10 (0.01%), with config/env overrides. diff --git a/docs/adr/0001-runner-interface.md b/docs/adr/0001-runner-interface.md new file mode 100644 index 0000000..3352337 --- /dev/null +++ b/docs/adr/0001-runner-interface.md @@ -0,0 +1,23 @@ +# ADR 0001: local JSON-RPC runner interface + +Status: accepted. + +Use newline-delimited JSON-RPC 2.0 over a Unix socket at `~/.bloxwap/run.sock`. +Each request/response occupies one UTF-8 line, with a bounded frame size. +Methods use resource.action names: `order.place`, `order.cancel`, `order.modify`, +`leverage.set`, `status.get`, `balance.get`, `positions.get`, `orders.get`, +`config.get`, `runner.shutdown`. Short CLI command names may be aliases. + +A socket supports multiple CLI clients and keeps signing and nonce ownership in +one process. The state directory is 0700 and the socket 0600; access is authorized +by the OS user. Never expose this protocol on a network address. Same-user malware +is outside this permission boundary. Validate every request independently. + +Stdin JSON-RPC is simple for one parent process but cannot support independent CLI +clients. Localhost HTTP supports clients and Windows but adds port discovery, +network exposure and authentication requirements. Neither is the default. + +On Windows use a named pipe with a name derived from the user's home directory, +with access restricted to the current user. Unix permission bits do not secure +Windows pipes: deployment must enforce the current-user ACL. Direct mode remains +available if the platform cannot establish a private local transport. diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..c3299a7 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,2527 @@ +# Command reference + +Generated from `agent-cli schema `. Do not edit by hand. + +Global flags: `--config PATH`, `--network mainnet|testnet`, `--pretty`, `--json OBJECT`. + +## config + +Get, set or validate local configuration + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "args": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "args" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## init + +Create agent and emit resumable master-wallet approvals + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "masterAddress": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{40}$" + }, + "validUntil": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "signature": { + "type": "string" + }, + "builderSignature": { + "type": "string" + }, + "qr": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "qr" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "masterAddress": { + "type": "string" + }, + "agentAddress": { + "type": "string" + }, + "expiry": { + "type": "number" + }, + "policyId": { + "type": "string" + }, + "approved": { + "type": "boolean" + }, + "requiresSignature": { + "type": "boolean" + }, + "approvals": {}, + "deeplink": { + "type": "string" + }, + "qrAscii": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "masterAddress", + "agentAddress", + "expiry", + "policyId", + "approved" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## fund + +Quote funding with address, QR and transparent fees + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "originChain": { + "default": 42161, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "originCurrency": { + "default": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "type": "string" + }, + "originDecimals": { + "default": 6, + "type": "integer", + "minimum": 0, + "maximum": 30 + }, + "amount": { + "default": "10", + "type": "string" + }, + "qr": { + "default": true, + "type": "boolean" + }, + "qrPath": { + "type": "string" + } + }, + "required": [ + "originChain", + "originCurrency", + "originDecimals", + "amount", + "qr" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "depositId": { + "type": "string" + }, + "depositAddress": { + "type": "string" + }, + "originChain": { + "type": "number" + }, + "originCurrency": { + "type": "string" + }, + "minDeposit": { + "type": "string" + }, + "expectedReceived": { + "type": "string" + }, + "expectedReceivedHuman": { + "type": "string" + }, + "fees": {}, + "builderFeeRate": { + "type": "string" + }, + "builderAddress": { + "type": "string" + }, + "expiresAt": { + "type": "number" + }, + "state": { + "type": "string" + }, + "signatureSteps": { + "type": "array", + "items": {} + }, + "qrAscii": { + "type": [ + "string", + "null" + ] + }, + "qrPath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "depositId", + "depositAddress", + "expectedReceived", + "fees", + "builderFeeRate", + "builderAddress", + "expiresAt", + "qrAscii" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## deposit + +Resume deposit steps or submit a master-wallet signature + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "depositId": { + "type": "string" + }, + "stepId": { + "type": "string" + }, + "signature": { + "type": "string" + } + }, + "required": [ + "depositId" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "depositId": { + "type": "string" + }, + "steps": { + "type": "array", + "items": {} + }, + "complete": { + "type": "boolean" + } + }, + "required": [] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## status + +Read account status + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "balance": { + "type": "object", + "properties": { + "accountValue": { + "type": "string" + }, + "accountValueUnits": { + "type": "string" + }, + "withdrawable": { + "type": "string" + }, + "marginUsed": { + "type": "string" + } + }, + "required": [ + "accountValue", + "accountValueUnits", + "withdrawable", + "marginUsed" + ] + }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "size": { + "type": "string" + }, + "entryPrice": { + "type": [ + "string", + "null" + ] + }, + "unrealizedPnl": { + "type": "string" + }, + "liquidationPrice": { + "type": [ + "string", + "null" + ] + }, + "leverage": { + "type": "string" + }, + "marginMode": { + "type": "string" + } + }, + "required": [ + "coin", + "size", + "leverage" + ] + } + }, + "agent": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "approved": { + "type": "boolean" + }, + "revoked": { + "type": "boolean" + }, + "expiry": { + "type": [ + "number", + "null" + ] + }, + "daysRemaining": { + "type": [ + "string", + "null" + ] + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [] + }, + "policy": { + "type": "object", + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + } + }, + "required": [] + }, + "deposits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "state": { + "enum": [ + "awaiting_funds", + "pending", + "settled", + "failed", + "refunded" + ] + } + }, + "required": [] + } + }, + "approvedMaxBuilderFee": { + "type": "string" + }, + "runner": { + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "balance", + "positions", + "agent", + "policy", + "deposits", + "approvedMaxBuilderFee", + "runner" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## balance + +Read account balance + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "accountValue": { + "type": "string" + }, + "accountValueUnits": { + "type": "string" + }, + "withdrawable": { + "type": "string" + }, + "marginUsed": { + "type": "string" + } + }, + "required": [ + "accountValue", + "accountValueUnits", + "withdrawable", + "marginUsed" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## positions + +Read account positions + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "size": { + "type": "string" + }, + "entryPrice": { + "type": [ + "string", + "null" + ] + }, + "unrealizedPnl": { + "type": "string" + }, + "liquidationPrice": { + "type": [ + "string", + "null" + ] + }, + "leverage": { + "type": "string" + }, + "marginMode": { + "type": "string" + } + }, + "required": [ + "coin", + "size", + "leverage" + ] + } + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## orders + +Read account orders + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "oid": { + "type": "string" + }, + "sz": { + "type": "string" + }, + "limitPx": { + "type": "string" + }, + "side": { + "enum": [ + "A", + "B" + ] + }, + "cloid": { + "type": "string" + } + }, + "required": [ + "coin", + "oid", + "sz", + "limitPx", + "side" + ] + } + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## order + +Place limit, IOC market or TP/SL order + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "side": { + "type": "string", + "enum": [ + "buy", + "sell" + ] + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + }, + "type": { + "default": "limit", + "type": "string", + "enum": [ + "limit", + "market", + "tp", + "sl" + ] + }, + "tif": { + "default": "Gtc", + "type": "string", + "enum": [ + "Gtc", + "Ioc", + "Alo" + ] + }, + "reduceOnly": { + "default": false, + "type": "boolean" + }, + "cloid": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{32}$" + }, + "triggerPrice": { + "type": "string" + }, + "slippage": { + "default": 0.01, + "type": "number", + "minimum": 0, + "maximum": 0.1 + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "required": [ + "coin", + "side", + "size", + "type", + "tif", + "reduceOnly", + "slippage" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "result": {}, + "builderFeeRate": { + "type": "string" + }, + "builderAddress": { + "type": "string" + }, + "cloid": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "result", + "builderFeeRate", + "builderAddress", + "cloid" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## cancel + +Cancel by oid/cloid, coin or all + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "oid": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "oids": { + "type": "array", + "items": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "cloid": { + "type": "string" + }, + "cloids": { + "type": "array", + "items": { + "type": "string" + } + }, + "coin": { + "type": "string" + }, + "all": { + "default": false, + "type": "boolean" + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "required": [ + "all" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "cancelled": { + "type": "array", + "items": { + "type": "string" + } + }, + "result": {} + }, + "required": [ + "cancelled", + "result" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## modify + +Modify price/size of single or batch resting orders + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "oid": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "cloid": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{32}$" + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + }, + "modifications": { + "type": "array", + "items": { + "type": "object", + "properties": { + "oid": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "cloid": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{32}$" + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "response": {} + }, + "required": [ + "status" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## leverage + +Set market leverage and cross/isolated margin mode + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "leverage": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "mode": { + "default": "cross", + "type": "string", + "enum": [ + "cross", + "isolated" + ] + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "required": [ + "coin", + "leverage", + "mode" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "response": {} + }, + "required": [ + "status" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## run + +Start persistent signing runner (foreground or detach) + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "detach": { + "default": false, + "type": "boolean" + }, + "daemon": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "detach", + "daemon" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "pid": { + "type": "integer" + }, + "startedAt": { + "type": "number" + }, + "network": { + "enum": [ + "mainnet", + "testnet" + ] + }, + "ready": { + "type": "boolean" + }, + "existing": { + "type": "boolean" + } + }, + "required": [ + "pid", + "startedAt", + "network" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## events + +Replay or follow JSON-line runner events + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "follow": { + "default": false, + "type": "boolean" + }, + "since": { + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "types": { + "default": [], + "type": "array", + "items": { + "type": "string", + "enum": [ + "fill", + "order_update", + "position_change", + "deposit", + "error", + "agent_expiry_warning", + "runner_state" + ] + } + } + }, + "required": [ + "follow", + "since", + "types" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "seq": { + "type": "integer", + "minimum": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "type": { + "enum": [ + "fill", + "order_update", + "position_change", + "deposit", + "error", + "agent_expiry_warning", + "runner_state" + ] + }, + "data": {} + }, + "required": [ + "seq", + "timestamp", + "type", + "data" + ] + } + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## logs + +Read/follow redacted JSON-line runner logs + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "follow": { + "default": false, + "type": "boolean" + }, + "level": { + "type": "string", + "enum": [ + "debug", + "info", + "warn", + "error" + ] + }, + "since": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "follow" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "description": "JSON-line stream of redacted log objects", + "type": "object" + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## rotate + +Approve new agent, hot-swap signer, then revoke old agent + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "signature": { + "type": "string" + }, + "revokeSignature": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "intent": { + "type": "string" + } + }, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "oldAgent": { + "type": "string" + }, + "newAgent": { + "type": "string" + }, + "swapped": { + "type": "boolean" + }, + "oldRevoked": { + "type": "boolean" + }, + "requiresSignature": { + "type": "boolean" + }, + "approval": {}, + "revokeApproval": {} + }, + "required": [ + "oldAgent", + "newAgent", + "swapped", + "oldRevoked" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## revoke + +Disable local agent and prepare master-signed exchange revocation + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "cancelOrders": { + "default": false, + "type": "boolean" + }, + "signature": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "intent": { + "type": "string" + } + }, + "required": [ + "cancelOrders" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "agentAddress": { + "type": "string" + }, + "locallyRevoked": { + "type": "boolean" + }, + "exchangeRevoked": { + "type": "boolean" + }, + "requiresSignature": { + "type": "boolean" + }, + "approval": {} + }, + "required": [ + "agentAddress", + "locallyRevoked", + "exchangeRevoked", + "requiresSignature", + "approval" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## audit + +Verify and query the hash-chained trading audit + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "cloid": { + "type": "string" + } + }, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "seq": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "cloid": { + "type": "string" + }, + "stage": { + "type": "string" + }, + "data": {}, + "previousHash": { + "type": "string" + }, + "hash": { + "type": "string" + } + }, + "required": [ + "seq", + "timestamp", + "intentId", + "stage", + "data", + "previousHash", + "hash" + ] + } + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## skill + +Install packaged version-matched skill for Claude Code or Codex + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "args": { + "default": [ + "install" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "target": { + "default": "codex", + "type": "string", + "enum": [ + "claude", + "codex" + ] + }, + "force": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "args", + "target", + "force" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "target": { + "enum": [ + "claude", + "codex" + ] + }, + "path": { + "type": "string" + }, + "version": { + "type": "string" + }, + "changed": { + "type": "boolean" + } + }, + "required": [ + "target", + "path", + "version", + "changed" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## market + +Discover market asset, precision, leverage ceiling and current mid + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "coin": { + "type": "string" + } + }, + "required": [ + "coin" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "asset": { + "type": "string" + }, + "szDecimals": { + "type": "string" + }, + "maxLeverage": { + "type": "string" + }, + "mid": { + "type": "string" + } + }, + "required": [ + "coin", + "asset", + "szDecimals", + "maxLeverage", + "mid" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..9079f46 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,24 @@ +# Configuration + +Default path: `~/.bloxwap/config.json`. `--config` selects another file. +All command JSON objects reject unknown fields to catch intent-changing typos. +Version 1 is validated on read and write; versionless config migrates to version 1. +Unknown versions fail closed. Writes use a private temporary file and atomic rename. + +`BLOXWAP_NETWORK`, `BLOXWAP_LOG_LEVEL`, `BLOXWAP_PRIVY_APP_ID` and +`BLOXWAP_MASTER_ADDRESS` override stored values in commands. `config get` shows +persisted values; `config validate` validates them without overrides. +Secrets use `env:BLOXWAP_PRIVY_APP_SECRET` and `env:BLOXWAP_AUTHORIZATION_KEY` +references; authorization keys may also use `file:/absolute/private/path`. + +The default builder address is `0x71b09A08257078A4d642F7Dd7315e656c837329a` +(Bloxwap), with `builder.fee:10`. Hyperliquid uses tenths of a basis point: +`f=10` = 1 bp = **0.01%**. Override with `config set builder.address 0x...` +and `config set builder.fee 10`, or `BLOXWAP_BUILDER_ADDRESS` and +`BLOXWAP_BUILDER_FEE` (environment overrides stored values for commands). +Fees must be integers from 0 to 100; `builder.fee:0` disables the fee. +`init` requests an external master-wallet `approveBuilderFee` signature for the +configured address/rate; submit it with `init --builder-signature 0x...`. +Each order checks the master-approved maximum before attaching `{b,f}`. +Fund and order JSON expose `builderAddress` and `builderFeeRate` (`"0.01%"` +by default). Funding reports the trading builder rate separately from Relay fees. diff --git a/docs/custody.md b/docs/custody.md new file mode 100644 index 0000000..ae8e6fa --- /dev/null +++ b/docs/custody.md @@ -0,0 +1,16 @@ +# Custody boundary + +The master wallet approves a separate Hyperliquid API agent. Its key stays in Privy; +the CLI holds a P-256 authorization key, never the master private key. Hyperliquid +agent permissions prevent withdrawals and master-only user-signed actions. + +The versioned Privy policy permits only EIP-712 Agent messages in the Hyperliquid +L1 domain (chain 1337, zero verifying contract, correct network source). Unmatched +RPC methods are denied by default. User-signed withdrawal and approval messages +have a different domain/type. Local dispatch allows only trading operations. + +Privy sees the L1 action hash, not its preimage: it cannot independently distinguish +an order from another L1 action or enforce notional limits inside that hash. +Local limits and exchange agent permissions supply those checks. Do not describe +this policy as independent server-side per-action/notional enforcement. Confirm +policy behavior in the credential-gated sandbox before production use. diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..cdc84fc --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,25 @@ +# Stable errors + +Failures always have `{ok:false,code,message,retryable,details}`; successes have +`{ok:true,data}`. Codes are append-only. Retry only when retryable is true, using +bounded backoff. Exit 2 means invalid configuration/input, 75 means temporary +failure, 1 means a permanent failure. + +| Code | Meaning | Retryable | Agent action | +| --- | --- | --- | --- | +| INSUFFICIENT_MARGIN | Account margin too low | false | Reduce size or fund | +| POLICY_REJECTED | Local or custody policy denied | false | Adjust intent | +| AGENT_EXPIRED | Agent expired | false | Rotate | +| AGENT_NOT_APPROVED | Agent or builder unapproved | false | Complete init | +| DEPOSIT_PENDING | Deposit unsettled | true | Poll status | +| NONCE_COLLISION | Nonce reused | true | Generate next nonce | +| RATE_LIMIT | Upstream throttled | true | Back off | +| INVALID_SIZE | Invalid amount | false | Correct size | +| INVALID_PRICE | Invalid price | false | Correct price | +| UNKNOWN_MARKET | Market absent | false | Refresh metadata | +| ORDER_NOT_FOUND | Order absent | false | Refresh orders | +| RUNNER_NOT_RUNNING | Runner unavailable | true | Start runner | +| CONFIG_INVALID | Invalid input/config | false | Correct configuration | +| NETWORK_ERROR | Transport failure | true | Back off | +| UPSTREAM_ERROR | Unrecognized service failure | false | Inspect redacted details | +| INTERNAL | Unexpected local failure | false | Inspect logs | diff --git a/docs/events.md b/docs/events.md new file mode 100644 index 0000000..a9c867a --- /dev/null +++ b/docs/events.md @@ -0,0 +1,12 @@ +# Events + +Each JSON line is `{seq,timestamp,type,data}`. Sequence numbers strictly increase +across restarts; timestamps are ISO UTC. Types: `fill` (upstream fill with tid), +Fill fees additionally have exact 8-decimal `feeUnits` strings. +`order_update`, `position_change`, `deposit` (id/state), `error` (normalized +failure), `agent_expiry_warning`, `runner_state`. Secret fields are redacted. + +`events --since 12` returns events after sequence 12. `--follow` is an explicit +streaming exception to the one-document command contract: each event is one JSON +line. Filter with `--types '["fill","error"]'`. Save the last processed sequence +and replay it after reconnect. Shutdown flushes the queue before releasing state. diff --git a/docs/funding.md b/docs/funding.md new file mode 100644 index 0000000..e802d0f --- /dev/null +++ b/docs/funding.md @@ -0,0 +1,18 @@ +# Funding and signatures + +Run `fund --amount 10 --origin-chain 42161` and show the returned QR/address to the +user. Inspect the origin token, minimum, fees and quote expiry before sending. +Amounts are decimal strings; HyperCore perps amounts use 8 decimals, EVM USDC 6. + +Some routes return two master-wallet signatures: a Relay nonce/deposit mapping +and a Hyperliquid deposit authorization. The exact payloads come from the quote; +never synthesize them or sign them using the trade-only agent. `fund` emits +`signatureSteps` with payloads and `requiresSignature`. Query `deposit --deposit-id ID` +to resume; submit each in order with `--step-id ID --signature 0x...`. Completed +steps are persisted immediately. Relay signature submission keeps the quoted +POST body intact and adds the signature as a query parameter, per the +[step execution contract](https://docs.relay.link/references/api/api_core_concepts/step-execution). Retrying a completed flow is harmless. + +Poll `status` until the deposit is settled; a bridge status alone is insufficient +without the corresponding Hyperliquid balance increase. Testnet uses Relay's +testnet API and may require a mocked quote if that route is unavailable. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..09f8315 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,9 @@ +# agent-cli documentation + +Use the [command reference](commands.md) for JSON schemas, [configuration](config.md) +for state/env settings, [funding](funding.md) for deposits/signatures, +[errors](errors.md) for recovery, [events](events.md) for streaming, +[custody](custody.md) and [security](security.md) for key boundaries. + +Development progress is recorded in [PROGRESS](PROGRESS.md). Documentation is +built with `npm run docs:generate` into `site/`; serve that directory locally. diff --git a/docs/issues/ROADMAP.md b/docs/issues/ROADMAP.md index ea85f6e..a047643 100644 --- a/docs/issues/ROADMAP.md +++ b/docs/issues/ROADMAP.md @@ -38,7 +38,7 @@ Locked decisions: hybrid custody (approveAgent once, Privy-managed agent, trade- - [ ] #34 `revoke` command ### Funding -- [ ] #13 Builder code / builder fee configuration +- [x] #13 Builder code / builder fee configuration - [ ] #14 Relay quote client for HyperCore deposit addresses - [ ] #15 USDC decimal conversion: 6 (EVM) vs 8 (HyperCore perps) - [ ] #16 Terminal QR rendering diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..1d3a76a --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,13 @@ +# Releases + +Add `.changeset/*.md` for user-visible changes. `npm run version:packages` applies +versioning/changelogs and synchronizes the packaged skill version. The version +workflow creates a version PR when manually invoked. + +A matching `vVERSION` tag builds/tests/packs and compiles Linux/macOS executables, +then publishes with npm provenance and attaches binaries, SKILL.md and changelog +to a generated-notes GitHub release. Enable repository variable +`BLOXWAP_PUBLISH=1` and configure `npm-release` environment plus `NPM_TOKEN` (or npm +trusted publishing) before using it. No publishing occurs without that gate. +Registry ownership, provenance and hosted CI execution need external credentials. +The implementation process does not push tags, publish packages or alter remotes. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..c3ec951 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,26 @@ +# Security and threat model + +State is always `~/.bloxwap`, directory 0700, config/auth keys/logs/events/audit 0600. +P-256 Privy authorization keys are PKCS8 base64 in private files or environment +references. The CLI does not import master keys, seeds or mnemonic phrases. +External master-wallet signatures are accepted only as 65-byte signatures. +Every CLI and RPC input is scanned before schema parsing so unknown fields cannot +smuggle key material. Hex 32-byte secrets and BIP39-like phrases are refused. + +Trust boundaries: master wallet → one-time Hyperliquid agent approval → Privy +managed wallet → restricted local signing runner. Read `custody.md` for the hash +preimage limitation of server policies. Anyone controlling this OS user can use +the authorization key and change limits. Socket/file modes exclude other users, +not same-user malware or root. Windows named pipes require current-user ACLs; +use direct mode in environments unable to enforce private IPC. + +Logs and error envelopes recursively scrub sensitive fields, long hex values, +Bearer tokens and known secret environment values. Signature hashes, never raw +signatures, appear in the audit chain. A hash chain detects edits inside a retained +journal; it cannot detect deletion of the entire journal without an external +checkpoint. Back up checkpoints separately for stronger evidence. + +Never put secrets in command args, repository files or issue reports. Environment +references avoid plaintext config but do not protect against privileged process +inspection. Revoke and rotate after suspected compromise. Unit tests use service +mocks; real signing tests require explicit testnet/sandbox environment gates. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5fed676 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,5 @@ +import tseslint from 'typescript-eslint'; +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**'] }, + ...tseslint.configs.recommended, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a0afefa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4988 @@ +{ + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@bloxwap/hyperliquid": "^0.6.0", + "@privy-io/node": "^0.35.0", + "@scure/bip39": "^2.4.0", + "qrcode": "^1.5.4", + "ws": "^8.22.0", + "zod": "^4.6.7" + }, + "bin": { + "agent-cli": "dist/cli.js" + }, + "devDependencies": { + "@changesets/cli": "^3.0.3", + "@types/node": "^26.6.5", + "@types/qrcode": "^1.5.6", + "@types/ws": "^8.18.2", + "eslint": "^9.39.5", + "marked": "^18.1.1", + "prettier": "^3.9.10", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.71.2", + "viem": "^2.57.5", + "vitest": "^5.0.4" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "devOptional": true + }, + "node_modules/@bloxwap/hyperliquid": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@bloxwap/hyperliquid/-/hyperliquid-0.6.0.tgz", + "integrity": "sha512-ryQya36sx5tD0xg9USFg+6krpMvPlHvc55p42FXJlCN6RTWO9obA6QknZpM1RRTPAl5w04Jj16kQxLcesEWl0Q==", + "dependencies": { + "@noble/hashes": "2.2.0", + "valibot": "1.4.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "hash-wasm": "4.12.0", + "tiny-secp256k1": "2.2.4" + } + }, + "node_modules/@changesets/apply-release-plan": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.1.1.tgz", + "integrity": "sha512-nk2Hbq3M9NeHMdaIRBDvawKrQJzXB9sGyBUE5yWnIKtvdcNCPj75zBekJYr0gTT2ecJH3eFeTrc8eisPImcv0Q==", + "dev": true, + "dependencies": { + "@changesets/config": "^4.0.1", + "@changesets/format": "^0.1.2", + "@changesets/git": "^4.0.1", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-7.0.0.tgz", + "integrity": "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==", + "dev": true, + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-1.0.0.tgz", + "integrity": "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/cli": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-3.0.3.tgz", + "integrity": "sha512-L1i67bMfkK6iC/kDejuNO4pFgTRcWIObsD6Qof2woP1I4M8tj17DGlcpRaTzTv1DYSP5EMwWdEtR8X0Pj5Jg3Q==", + "dev": true, + "dependencies": { + "@changesets/apply-release-plan": "^8.1.1", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.1", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.1", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.1", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.1", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.1" + }, + "bin": { + "changeset": "bin.js" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" + } + }, + "node_modules/@changesets/cli/node_modules/cac": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.1.0.tgz", + "integrity": "sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA==", + "dev": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@changesets/cli/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@changesets/config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-4.0.1.tgz", + "integrity": "sha512-PxpxSQLQSLDjceAgRq+4FWlU+HflDIw03mmPVipxoQx9xrXFUjmqTy3SE+n/2t+DqPdpPiLWVr5NRiLZvjyLwg==", + "dev": true, + "dependencies": { + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.7" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-1.0.0.tgz", + "integrity": "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==", + "dev": true, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/format/-/format-0.1.2.tgz", + "integrity": "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==", + "dev": true, + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-3.0.0.tgz", + "integrity": "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-4.0.1.tgz", + "integrity": "sha512-6vWpIAC4LkpmlqaIu37ViT5enyd9+uBwAiGqCGhASvkSHBgx4PrtTGXWTMFYpDmZbjgyonyVgMciPrW4MqtJsA==", + "dev": true, + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@changesets/parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-1.0.0.tgz", + "integrity": "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/pre": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-3.0.0.tgz", + "integrity": "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==", + "dev": true, + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/read": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-1.0.1.tgz", + "integrity": "sha512-nzvSC6RcTiWf/dsuwZFXpLzGGsRHYCL68U3uHV7srsulU93aVWY7u2GEJiDZ+4GIIElfaW5EqtKC0UHroY+LTQ==", + "dev": true, + "dependencies": { + "@changesets/git": "^4.0.1", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-1.0.0.tgz", + "integrity": "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-7.0.0.tgz", + "integrity": "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==", + "dev": true, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/write": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-1.0.1.tgz", + "integrity": "sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA==", + "dev": true, + "dependencies": { + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "dev": true, + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "dev": true, + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hpke/chacha20poly1305": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.8.0.tgz", + "integrity": "sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/common": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.1.tgz", + "integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz", + "integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@manypkg/find-root": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-3.1.0.tgz", + "integrity": "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==", + "dev": true, + "dependencies": { + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-3.1.0.tgz", + "integrity": "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==", + "dev": true, + "dependencies": { + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-2.1.2.tgz", + "integrity": "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==", + "dev": true, + "dependencies": { + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", + "integrity": "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==", + "dev": true, + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, + "node_modules/@privy-io/node": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@privy-io/node/-/node-0.35.0.tgz", + "integrity": "sha512-NhttAOmjgwtQzBoTmRCWzY5ZTTJV1WqPUmasyEbi9XfFd1LccpjHiNB+AUCeRXpY0FC5Bt/3Is7xJEmCX3LKNA==", + "dependencies": { + "@hpke/chacha20poly1305": "^1.7.1", + "@hpke/core": "^1.7.5", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@scure/base": "^1.2.5", + "canonicalize": "^2.1.0", + "jose": "^6.1.0", + "lru-cache": "^11.1.0", + "svix": "^1.92.2" + }, + "peerDependencies": { + "@solana/kit": "^5.1.0", + "@x402/evm": "^2.3.0", + "@x402/fetch": "^2.3.0", + "@x402/svm": "^2.3.0", + "viem": "^2.44.2" + }, + "peerDependenciesMeta": { + "@solana/kit": { + "optional": true + }, + "@x402/evm": { + "optional": true + }, + "@x402/fetch": { + "optional": true + }, + "@x402/svm": { + "optional": true + }, + "viem": { + "optional": true + } + } + }, + "node_modules/@privy-io/node/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz", + "integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz", + "integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz", + "integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz", + "integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz", + "integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz", + "integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz", + "integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz", + "integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz", + "integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz", + "integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz", + "integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz", + "integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz", + "integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz", + "integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz", + "integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz", + "integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz", + "integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz", + "integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz", + "integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz", + "integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz", + "integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz", + "integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz", + "integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz", + "integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz", + "integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "devOptional": true, + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.4.tgz", + "integrity": "sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.4", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.4.tgz", + "integrity": "sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 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, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "devOptional": true + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "optional": true + }, + "node_modules/human-id": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.2.tgz", + "integrity": "sha512-Z+8unD6em/MJwyrkWQ2M3wygRGI1+MztbutiNwk1DCuEPXOZRWGjtcMPZYP1exY9iNZMhd7ZOSMQSo9WYtiQRw==", + "dev": true, + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/launch-editor": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.2.tgz", + "integrity": "sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "engines": { + "node": "20 || >=22" + } + }, + "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, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.1.tgz", + "integrity": "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ox": { + "version": "0.14.56", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.56.tgz", + "integrity": "sha512-Feinlj+deWEam6RIwurRE+eHZ2N3hoUb3AAjQ10BmxRCYIymfqNkv0eZ+4ln4GkgDmSGLD2rGtTMqsFYSVDC6w==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "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 + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "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" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, + "node_modules/rollup": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz", + "integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.6", + "@rollup/rollup-android-arm64": "4.64.6", + "@rollup/rollup-darwin-arm64": "4.64.6", + "@rollup/rollup-darwin-x64": "4.64.6", + "@rollup/rollup-freebsd-arm64": "4.64.6", + "@rollup/rollup-freebsd-x64": "4.64.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.6", + "@rollup/rollup-linux-arm-musleabihf": "4.64.6", + "@rollup/rollup-linux-arm64-gnu": "4.64.6", + "@rollup/rollup-linux-arm64-musl": "4.64.6", + "@rollup/rollup-linux-loong64-gnu": "4.64.6", + "@rollup/rollup-linux-loong64-musl": "4.64.6", + "@rollup/rollup-linux-ppc64-gnu": "4.64.6", + "@rollup/rollup-linux-ppc64-musl": "4.64.6", + "@rollup/rollup-linux-riscv64-gnu": "4.64.6", + "@rollup/rollup-linux-riscv64-musl": "4.64.6", + "@rollup/rollup-linux-s390x-gnu": "4.64.6", + "@rollup/rollup-linux-x64-gnu": "4.64.6", + "@rollup/rollup-linux-x64-musl": "4.64.6", + "@rollup/rollup-openbsd-x64": "4.64.6", + "@rollup/rollup-openharmony-arm64": "4.64.6", + "@rollup/rollup-win32-arm64-msvc": "4.64.6", + "@rollup/rollup-win32-ia32-msvc": "4.64.6", + "@rollup/rollup-win32-x64-gnu": "4.64.6", + "@rollup/rollup-win32-x64-msvc": "4.64.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svix": { + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.99.1.tgz", + "integrity": "sha512-JfpvbAg5iT5NagDAfOq3e6Ci6NbOMbMm6C3DnfDBB60m2JDK+Z2hXPE9XNAmutb53DCdPoih1V70IbklZnX09A==", + "dependencies": { + "standardwebhooks": "1.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "optional": true, + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "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 + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.57.5", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.57.5.tgz", + "integrity": "sha512-BaUdS5lJJTxr6hJX17K0AjHbgUhsolsqH8CCbTq1WMunW/I/Siwc9VrTwccCcNPppgqa/gY4TqK8Zl8LLmlTlw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.56", + "ws": "8.21.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "devOptional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.4.tgz", + "integrity": "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.4", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.4", + "@vitest/browser-preview": "5.0.4", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.4", + "@vitest/coverage-v8": "5.0.4", + "@vitest/ui": "5.0.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a508694 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "type": "module", + "bin": { + "agent-cli": "dist/cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "files": [ + "dist", + "SKILL.md", + "policies" + ], + "scripts": { + "build": "node scripts/generate-skill.mjs && tsup src/cli.ts --format esm --clean && node scripts/finalize-build.mjs", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "docs:generate": "npm run build && node scripts/generate-docs.mjs", + "test:e2e": "node scripts/e2e-testnet.mjs", + "test:package": "node scripts/package-smoke.mjs", + "build:binaries": "node scripts/generate-skill.mjs && node scripts/build-binaries.mjs", + "changeset": "changeset", + "version:packages": "changeset version && node scripts/sync-skill-version.mjs && node scripts/generate-skill.mjs", + "test:sandbox": "vitest run --config vitest.integration.config.ts" + }, + "devDependencies": { + "@changesets/cli": "^3.0.3", + "@types/node": "^26.6.5", + "@types/qrcode": "^1.5.6", + "@types/ws": "^8.18.2", + "eslint": "^9.39.5", + "marked": "^18.1.1", + "prettier": "^3.9.10", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.71.2", + "viem": "^2.57.5", + "vitest": "^5.0.4" + }, + "dependencies": { + "@bloxwap/hyperliquid": "^0.6.0", + "@privy-io/node": "^0.35.0", + "@scure/bip39": "^2.4.0", + "qrcode": "^1.5.4", + "ws": "^8.22.0", + "zod": "^4.6.7" + }, + "description": "Agent-native Hyperliquid trading CLI with Privy custody and Relay funding", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "overrides": { + "esbuild": "^0.28.1" + } +} diff --git a/policies/agent-trade-only.json b/policies/agent-trade-only.json new file mode 100644 index 0000000..353d662 --- /dev/null +++ b/policies/agent-trade-only.json @@ -0,0 +1,8 @@ +{ + "version":"1.0","name":"agent-cli trade-only v1","chain_type":"ethereum", + "rules":[{"name":"Hyperliquid L1 agent domain","method":"eth_signTypedData_v4","action":"ALLOW","conditions":[ + {"field_source":"ethereum_typed_data_domain","field":"chainId","operator":"eq","value":"1337"}, + {"field_source":"ethereum_typed_data_domain","field":"verifyingContract","operator":"eq","value":"0x0000000000000000000000000000000000000000"}, + {"field_source":"ethereum_typed_data_message","field":"source","operator":"eq","value":"b","typed_data":{"primary_type":"Agent","types":{"Agent":[{"name":"source","type":"string"},{"name":"connectionId","type":"bytes32"}]}}} + ]}] +} diff --git a/scripts/build-binaries.mjs b/scripts/build-binaries.mjs new file mode 100644 index 0000000..1a6b8aa --- /dev/null +++ b/scripts/build-binaries.mjs @@ -0,0 +1,19 @@ +import { execFileSync } from 'node:child_process'; +import { mkdirSync } from 'node:fs'; +mkdirSync('binaries', { recursive: true }); +const targets = process.argv.slice(2); +for (const target of targets.length + ? targets + : ['bun-linux-x64', 'bun-darwin-arm64']) + execFileSync( + 'bun', + [ + 'build', + 'src/cli.ts', + '--compile', + `--target=${target}`, + '--outfile', + `binaries/agent-cli-${target.replace('bun-', '')}`, + ], + { stdio: 'inherit' }, + ); diff --git a/scripts/check-commit.sh b/scripts/check-commit.sh new file mode 100644 index 0000000..6180041 --- /dev/null +++ b/scripts/check-commit.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +npm run build +npm run lint +npm run typecheck +npm test +git add . +git commit -m "$1" diff --git a/scripts/e2e-relay-hook.mjs b/scripts/e2e-relay-hook.mjs new file mode 100644 index 0000000..da1f9f0 --- /dev/null +++ b/scripts/e2e-relay-hook.mjs @@ -0,0 +1,27 @@ +// Loaded only by the explicit testnet e2e harness, never by the packaged CLI. +const originalFetch = globalThis.fetch; +globalThis.fetch = async (url, init) => { + if (String(url).startsWith('https://api.testnets.relay.link/')) { + if (String(url).includes('/quote/')) + return Response.json({ + steps: [ + { + id: 'deposit', + kind: 'transaction', + requestId: 'mock-testnet-deposit', + depositAddress: '0x' + 'b'.repeat(40), + items: [{ status: 'incomplete', data: {} }], + }, + ], + fees: {}, + details: { + currencyOut: { amount: '0', currency: { decimals: 8 } }, + expiresAt: Date.now() + 60000, + }, + }); + if (String(url).includes('/intents/status')) + return Response.json({ status: 'success' }); + throw new Error('Unexpected Relay mock endpoint'); + } + return originalFetch(url, init); +}; diff --git a/scripts/e2e-testnet.mjs b/scripts/e2e-testnet.mjs new file mode 100644 index 0000000..b332c55 --- /dev/null +++ b/scripts/e2e-testnet.mjs @@ -0,0 +1,135 @@ +import { execFileSync } from 'node:child_process'; +import { mkdirSync, copyFileSync, existsSync, writeFileSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { homedir } from 'node:os'; +import { privateKeyToAccount } from 'viem/accounts'; +if ( + process.env.BLOXWAP_E2E !== '1' || + !process.env.BLOXWAP_TEST_MASTER_KEY || + !process.env.BLOXWAP_PRIVY_APP_ID || + !process.env.BLOXWAP_PRIVY_APP_SECRET +) { + console.log( + 'SKIP: set BLOXWAP_E2E=1 and dedicated testnet master/Privy credentials', + ); + process.exit(0); +} +if (!['true', '1'].includes(process.env.CI ?? '')) + throw new Error( + 'Master-key e2e signing is restricted to a dedicated CI account', + ); +const account = privateKeyToAccount(process.env.BLOXWAP_TEST_MASTER_KEY); +const config = join(homedir(), '.bloxwap', 'e2e-config.json'); +if (existsSync(config)) + throw new Error( + 'Refusing to replace an existing e2e config; use a clean CI account', + ); +const hook = resolve('scripts/e2e-relay-hook.mjs'); +const env = { + ...process.env, + NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${hook}`, +}; +const run = (name, input = {}) => { + const raw = execFileSync( + process.execPath, + [ + 'dist/cli.js', + name, + '--config', + config, + '--network', + 'testnet', + '--json', + JSON.stringify(input), + ], + { input: '', encoding: 'utf8', env }, + ); + const result = JSON.parse(raw); + if (!result.ok) throw new Error(JSON.stringify(result)); + return result.data; +}; +const sign = (payload) => account.signTypedData(payload.typedData); +const info = async (type, params = {}) => + ( + await fetch('https://api.hyperliquid-testnet.xyz/info', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ type, ...params }), + }) + ).json(); +try { + run('config', { + args: ['set', 'privyAppId', process.env.BLOXWAP_PRIVY_APP_ID], + }); + const init = run('init', { masterAddress: account.address, qr: false }); + if (!init.approved) + run('init', { signature: await sign(init.approvals.agent), qr: false }); + const state = await info('clearinghouseState', { user: account.address }); + if (Number(state.withdrawable) < 30) + throw new Error( + 'Dedicated testnet account needs at least 30 test USDC; no real funding is performed', + ); + run('fund', { amount: '10', qr: false }); + const status = run('status'); + if (status.deposits.some((d) => d.state !== 'settled')) + throw new Error('Mocked Relay funding did not settle'); + run('run', { detach: true }); + const meta = await info('meta'); + const market = meta.universe.find((m) => m.name === 'BTC'); + const mids = await info('allMids'); + const mid = Number(mids.BTC); + const size = ( + Math.ceil((20 / mid) * 10 ** market.szDecimals) / + 10 ** market.szDecimals + ).toFixed(market.szDecimals); + const cloid = '0x' + Date.now().toString(16).padStart(32, '0'); + const order = run('order', { + coin: 'BTC', + side: 'buy', + size, + price: String(Math.floor(mid * 0.7)), + cloid, + intentId: 'nightly-lifecycle', + }); + const oid = order.result.response.data.statuses[0].resting?.oid; + if (!oid) throw new Error('Expected resting test order'); + run('modify', { oid, price: String(Math.floor(mid * 0.69)) }); + run('cancel', { oid }); + run('events', { since: 0 }); + const rotation = run('rotate'); + const swapped = run('rotate', { signature: await sign(rotation.approval) }); + run('rotate', { revokeSignature: await sign(swapped.revokeApproval) }); + const revoke = run('revoke', { cancelOrders: true }); + run('revoke', { signature: await sign(revoke.approval) }); + run('audit', { intent: 'nightly-lifecycle' }); + console.log( + 'PASS: testnet init/fund/run/order/modify/cancel/events/rotate/revoke', + ); +} finally { + mkdirSync('artifacts', { recursive: true }); + for (const file of ['events.jsonl', 'audit.jsonl']) + if (existsSync(join(homedir(), '.bloxwap', file))) + copyFileSync(join(homedir(), '.bloxwap', file), join('artifacts', file)); + // A failed run records its stage via journals; credentials are never exported. + writeFileSync('artifacts/network.txt', 'testnet\n'); + const { createConnection } = await import('node:net'); + await new Promise((resolveStop) => { + const socket = createConnection(join(homedir(), '.bloxwap', 'run.sock')); + socket.on('connect', () => + socket.end( + JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'shutdown', + params: {}, + }) + '\n', + ), + ); + socket.on('error', resolveStop); + socket.on('close', resolveStop); + socket.setTimeout(5000, () => { + socket.destroy(); + resolveStop(); + }); + }); +} diff --git a/scripts/finalize-build.mjs b/scripts/finalize-build.mjs new file mode 100644 index 0000000..388c82f --- /dev/null +++ b/scripts/finalize-build.mjs @@ -0,0 +1,2 @@ +import { chmodSync } from 'node:fs'; +chmodSync('dist/cli.js', 0o755); diff --git a/scripts/generate-docs.mjs b/scripts/generate-docs.mjs new file mode 100644 index 0000000..1e1c339 --- /dev/null +++ b/scripts/generate-docs.mjs @@ -0,0 +1,27 @@ +import { execFileSync } from 'node:child_process'; +import { writeFileSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'; +import { marked } from 'marked'; +const invoke = (args) => + JSON.parse( + execFileSync(process.execPath, ['dist/cli.js', ...args], { + input: '', + encoding: 'utf8', + }), + ).data; +const commands = invoke(['--help']).commands; +let reference = + '# Command reference\n\nGenerated from `agent-cli schema `. Do not edit by hand.\n\nGlobal flags: `--config PATH`, `--network mainnet|testnet`, `--pretty`, `--json OBJECT`.\n\n'; +for (const command of commands) + reference += `## ${command.name}\n\n${command.description}\n\n\`\`\`json\n${JSON.stringify(invoke(['schema', command.name]), null, 2)}\n\`\`\`\n\n`; +writeFileSync('docs/commands.md', reference); +mkdirSync('site', { recursive: true }); +for (const name of readdirSync('docs').filter((name) => name.endsWith('.md'))) { + const content = marked(readFileSync(`docs/${name}`, 'utf8')).replace( + /href="([^"#]+)\.md/g, + 'href="$1.html', + ); + writeFileSync( + `site/${name.replace('.md', '.html')}`, + `agent-cli${content}`, + ); +} diff --git a/scripts/generate-skill.mjs b/scripts/generate-skill.mjs new file mode 100644 index 0000000..da51c99 --- /dev/null +++ b/scripts/generate-skill.mjs @@ -0,0 +1,9 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +const { version } = JSON.parse(readFileSync('package.json', 'utf8')); +const content = readFileSync('SKILL.md', 'utf8'); +if (!content.includes(` version: "${version}"`)) + throw new Error('Skill version differs from CLI'); +writeFileSync( + 'src/core/skill-content.ts', + `// Generated from SKILL.md and package.json.\nexport const cliVersion=${JSON.stringify(version)};\nexport const skillContent=${JSON.stringify(content)};\n`, +); diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs new file mode 100644 index 0000000..9b21ffe --- /dev/null +++ b/scripts/package-smoke.mjs @@ -0,0 +1,53 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +const directory = mkdtempSync(join(tmpdir(), 'bloxwap-package-')); +try { + const packed = JSON.parse( + execFileSync('npm', ['pack', '--json', '--pack-destination', directory], { + encoding: 'utf8', + }), + )[0]; + if (!packed.files.some((file) => file.path === 'SKILL.md')) + throw new Error('Package missing skill'); + execFileSync( + 'npm', + [ + 'install', + '--global', + '--prefix', + join(directory, 'global'), + join(directory, packed.filename), + ], + { stdio: 'pipe' }, + ); + const bin = + process.platform === 'win32' + ? join(directory, 'global', 'agent-cli.cmd') + : join(directory, 'global', 'bin', 'agent-cli'); + const help = JSON.parse( + execFileSync(bin, ['--help'], { input: '', encoding: 'utf8' }), + ); + if ( + !help.ok || + !help.data.commands.some((command) => command.name === 'order') + ) + throw new Error('Global CLI smoke failed'); + const schema = JSON.parse( + execFileSync(bin, ['schema', 'order'], { input: '', encoding: 'utf8' }), + ); + if (!schema.ok) throw new Error('Schema smoke failed'); + if (process.env.BLOXWAP_SMOKE_BUN === '1') { + const bun = JSON.parse( + execFileSync('bun', [resolve('dist/cli.js'), '--help'], { + input: '', + encoding: 'utf8', + }), + ); + if (!bun.ok) throw new Error('Bun smoke failed'); + } + console.log('PASS: packed global install, CLI/schema and optional Bun smoke'); +} finally { + rmSync(directory, { recursive: true, force: true }); +} diff --git a/scripts/sync-skill-version.mjs b/scripts/sync-skill-version.mjs new file mode 100644 index 0000000..6442d96 --- /dev/null +++ b/scripts/sync-skill-version.mjs @@ -0,0 +1,7 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +const { version } = JSON.parse(readFileSync('package.json', 'utf8')); +const skill = readFileSync('SKILL.md', 'utf8'); +writeFileSync( + 'SKILL.md', + skill.replace(/(metadata:\n version: )"[^"]+"/, `$1"${version}"`), +); diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..4f6ee99 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,42 @@ +#!/usr/bin/env node +import { dispatch, setRouting } from './core/router.js'; +import './commands/config.js'; +import './commands/init.js'; +import './commands/fund.js'; +import './commands/deposit.js'; +import './commands/read.js'; +import './commands/order.js'; +import './commands/cancel.js'; +import './commands/modify.js'; +import './commands/leverage.js'; +import './commands/run.js'; +import './commands/events.js'; +import './commands/logs.js'; +import './commands/rotate.js'; +import './commands/revoke.js'; +import './commands/audit.js'; +import './commands/skill.js'; +import './commands/market.js'; +import { routeToRunner } from './runner/rpc.js'; +setRouting(routeToRunner); +import { AgentCliError, normalizeError } from './errors/index.js'; +try { + let stdin = ''; + if (!process.stdin.isTTY) + for await (const chunk of process.stdin) { + stdin += String(chunk); + if (Buffer.byteLength(stdin) > 1_048_576) + throw new AgentCliError('CONFIG_INVALID', 'JSON input exceeds 1 MiB'); + } + const result = await dispatch(process.argv.slice(2), stdin); + if (!result.context.streaming || !result.body.ok) + process.stdout.write( + JSON.stringify(result.body, null, result.context.pretty ? 2 : undefined) + + '\n', + ); + process.exitCode = result.exitCode; +} catch (error) { + const normalized = normalizeError(error); + process.stdout.write(JSON.stringify(normalized.toJSON()) + '\n'); + process.exitCode = normalized.exitCode; +} diff --git a/src/commands/audit.ts b/src/commands/audit.ts new file mode 100644 index 0000000..1b2f693 --- /dev/null +++ b/src/commands/audit.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { readAudit, verifyAudit } from '../core/audit.js'; +register('audit', { + description: 'Verify and query the hash-chained trading audit', + schema: z.object({ + intent: z.string().optional(), + intentId: z.string().optional(), + cloid: z.string().optional(), + }), + execute: async (input) => { + const entries = await readAudit(); + verifyAudit(entries); + return entries.filter( + (entry) => + ((!input.intent && !input.intentId) || + entry.intentId === (input.intentId ?? input.intent)) && + (!input.cloid || entry.cloid === input.cloid), + ); + }, +}); diff --git a/src/commands/cancel.ts b/src/commands/cancel.ts new file mode 100644 index 0000000..49e7e17 --- /dev/null +++ b/src/commands/cancel.ts @@ -0,0 +1,69 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import type { Config } from '../core/config.js'; +import { + assertCanTrade, + tradingServices, + exchangeAction, + type TradingServices, +} from '../core/trading.js'; +import { AgentCliError } from '../errors/index.js'; +export const cancelSchema = z.object({ + oid: z.number().int().positive().optional(), + oids: z.array(z.number().int().positive()).optional(), + cloid: z.string().optional(), + cloids: z.array(z.string()).optional(), + coin: z.string().optional(), + all: z.boolean().default(false), + intent: z.string().optional(), +}); +export async function cancelOrders( + config: Config, + input: z.infer, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const orders = await clients.orders(); + const oids = input.oids ?? (input.oid ? [input.oid] : []), + cloids = input.cloids ?? (input.cloid ? [input.cloid] : []); + if (!input.all && !input.coin && !oids.length && !cloids.length) + throw new AgentCliError('CONFIG_INVALID', 'Select oid, cloid, coin or all'); + for (const oid of oids) + if (!orders.some((o) => o.oid === oid)) + throw new AgentCliError('ORDER_NOT_FOUND', `Order ${oid} absent`); + for (const cloid of cloids) + if (!orders.some((o) => o.cloid === cloid)) + throw new AgentCliError('ORDER_NOT_FOUND', `Order ${cloid} absent`); + const selected = orders.filter( + (o) => + (!input.coin || o.coin === input.coin) && + (input.all || + (input.coin && !oids.length && !cloids.length) || + oids.includes(o.oid) || + (!!o.cloid && cloids.includes(o.cloid))), + ); + const cancels = await Promise.all( + selected.map(async (o) => ({ + a: (await clients.market(o.coin)).asset, + o: o.oid, + })), + ); + if (!cancels.length) return { cancelled: [], result: null }; + return { + cancelled: selected.map((o) => String(o.oid)), + result: await exchangeAction('cancel', () => + clients.exchange.cancel({ cancels }), + ), + }; +} +register('cancel', { + description: 'Cancel by oid/cloid, coin or all', + schema: cancelSchema, + execute: async (input, context) => + cancelOrders( + await commandConfig(context), + input as z.infer, + ), +}); diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..dbaa960 --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { loadConfig, saveConfig, migrate } from '../core/config.js'; +import { AgentCliError } from '../errors/index.js'; +register('config', { + description: 'Get, set or validate local configuration', + schema: z.object({ args: z.array(z.string()).default([]) }), + execute: async ({ args }, context) => { + const [action = 'get', key, value] = args as string[]; + const config = await loadConfig(context.configPath, {}); + if (action === 'validate') return { valid: true, version: config.version }; + if (action === 'get') + return key + ? (key + .split('.') + .reduce( + (v, k) => (v as Record)?.[k], + config, + ) ?? null) + : config; + if (action !== 'set' || !key || value === undefined) + throw new AgentCliError('CONFIG_INVALID', 'Use config set key value'); + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + parsed = value; + } + const updated = structuredClone(config) as Record; + const parts = key.split('.'); + let cursor = updated; + for (const part of parts.slice(0, -1)) { + if ( + !Object.hasOwn(cursor, part) || + !cursor[part] || + typeof cursor[part] !== 'object' + ) + throw new AgentCliError('CONFIG_INVALID', 'Unknown config key'); + cursor = cursor[part] as Record; + } + if ( + ['__proto__', 'constructor', 'prototype'].some((p) => parts.includes(p)) + ) + throw new AgentCliError('CONFIG_INVALID', 'Invalid key'); + cursor[parts.at(-1)!] = parsed; + const validated = migrate(updated); + await saveConfig(validated, context.configPath); + return validated; + }, +}); diff --git a/src/commands/deposit.ts b/src/commands/deposit.ts new file mode 100644 index 0000000..4cb227b --- /dev/null +++ b/src/commands/deposit.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { readDeposits } from '../funding/store.js'; +import { + signatureSteps, + submitDepositSignature, +} from '../funding/signatures.js'; +import { AgentCliError } from '../errors/index.js'; +register('deposit', { + description: 'Resume deposit steps or submit a master-wallet signature', + schema: z.object({ + depositId: z.string(), + stepId: z.string().optional(), + signature: z.string().optional(), + }), + execute: async (input) => { + if (input.signature && input.stepId) + return submitDepositSignature( + input.depositId as string, + input.stepId as string, + input.signature as string, + ); + const deposit = (await readDeposits()).find( + (d) => d.id === input.depositId, + ); + if (!deposit) throw new AgentCliError('CONFIG_INVALID', 'Unknown deposit'); + return { depositId: deposit.id, steps: signatureSteps(deposit) }; + }, +}); diff --git a/src/commands/events.ts b/src/commands/events.ts new file mode 100644 index 0000000..7983178 --- /dev/null +++ b/src/commands/events.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { + readEvents, + followEvents, + eventTypes, + type EventType, +} from '../runner/events.js'; +register('events', { + description: 'Replay or follow JSON-line runner events', + schema: z.object({ + follow: z.boolean().default(false), + since: z.number().int().nonnegative().default(0), + types: z.array(z.enum(eventTypes)).default([]), + }), + execute: async (input, context) => { + if (!input.follow) + return readEvents( + undefined, + input.since as number, + input.types as EventType[], + ); + context.streaming = true; + const abort = new AbortController(); + const stop = () => abort.abort(); + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + try { + await followEvents({ + since: input.since as number, + types: input.types as EventType[], + signal: abort.signal, + write: (event) => process.stdout.write(JSON.stringify(event) + '\n'), + }); + } finally { + process.off('SIGINT', stop); + process.off('SIGTERM', stop); + } + return null; + }, +}); diff --git a/src/commands/fund.ts b/src/commands/fund.ts new file mode 100644 index 0000000..b78fe27 --- /dev/null +++ b/src/commands/fund.ts @@ -0,0 +1,128 @@ +import { collateralBalance } from '../core/account.js'; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { loadConfig, type Config } from '../core/config.js'; +import { hlInfo } from '../core/http.js'; +import { RelayClient, PERPS_USDC, parseQuote } from '../funding/relay.js'; +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +import { renderQr } from '../funding/qr.js'; +import { readDeposits, writeDeposits } from '../funding/store.js'; +import { signatureSteps } from '../funding/signatures.js'; +import { builderRate } from '../core/builder.js'; +import { AgentCliError } from '../errors/index.js'; +export const fundSchema = z.object({ + originChain: z.number().int().positive().default(42161), + originCurrency: z + .string() + .default('0xaf88d065e77c8cC2239327C5EDb3A432268e5831'), + originDecimals: z.number().int().min(0).max(30).default(6), + amount: z.string().default('10'), + qr: z.boolean().default(true), + qrPath: z.string().optional(), +}); +export async function fund( + config: Config, + input: z.infer, + dependencies: { + relay?: RelayClient; + path?: string; + baseline?: () => Promise; + qr?: typeof renderQr; + } = {}, +) { + if (!config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Initialize master address'); + const amount = parseUsdc(input.amount, input.originDecimals); + if (amount <= 0n) + throw new AgentCliError('INVALID_SIZE', 'Positive funding amount required'); + const route = JSON.stringify([ + config.network, + config.masterAddress, + input.originChain, + input.originCurrency, + amount.toString(), + ]); + const deposits = await readDeposits(dependencies.path); + let deposit = deposits.find( + (d) => + d.route === route && + d.expiresAt > Date.now() && + d.state === 'awaiting_funds', + ); + if (!deposit) { + const quote = await ( + dependencies.relay ?? + new RelayClient( + config.network === 'testnet' + ? 'https://api.testnets.relay.link' + : 'https://api.relay.link', + ) + ).quote({ + user: config.masterAddress, + recipient: config.masterAddress, + originChainId: input.originChain, + originCurrency: input.originCurrency, + amount: amount.toString(), + destinationChainId: 1337, + destinationCurrency: PERPS_USDC, + useDepositAddress: true, + tradeType: 'EXACT_INPUT', + }); + const parsed = parseQuote(quote); + if (amount < BigInt(parsed.minAmount)) + throw new AgentCliError('INVALID_SIZE', 'Below minimum deposit', { + minimum: parsed.minAmount, + }); + const baseline = dependencies.baseline + ? await dependencies.baseline() + : collateralBalance( + await hlInfo(config.network, 'clearinghouseState', { + user: config.masterAddress, + }), + ); + deposit = { + id: parsed.depositId, + network: config.network, + recipient: config.masterAddress, + route, + quote, + expiresAt: parsed.expiresAt, + state: 'awaiting_funds', + baseline, + createdAt: Date.now(), + completedSteps: [], + }; + deposits.push(deposit); + await writeDeposits(deposits, dependencies.path); + } + const parsed = parseQuote(deposit.quote); + const received = formatUsdc( + BigInt(parsed.expectedReceived), + parsed.receivedDecimals, + ); + return { + ...parsed, + expectedReceived: parseUsdc(received, 8).toString(), + expectedReceivedHuman: received, + originChain: input.originChain, + originCurrency: input.originCurrency, + minDeposit: formatUsdc(BigInt(parsed.minAmount), input.originDecimals), + builderFeeRate: builderRate(config), + builderAddress: config.builder.address, + state: deposit.state, + signatureSteps: signatureSteps(deposit), + ...(await (dependencies.qr ?? renderQr)(parsed.depositAddress, { + enabled: input.qr, + path: input.qrPath, + })), + }; +} +register('fund', { + description: 'Quote funding with address, QR and transparent fees', + schema: fundSchema, + execute: async (input, context) => { + const config = await loadConfig(context.configPath); + if (context.network) config.network = context.network; + return fund(config, input as z.infer); + }, +}); diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 0000000..eddf838 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,123 @@ +import { z } from 'zod'; +import { renderQr } from '../funding/qr.js'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { register } from '../core/router.js'; +import { + loadConfig, + saveConfig, + atomicWrite, + stateDir, +} from '../core/config.js'; +import { privyClient, createAgent } from '../wallet/privy.js'; +import { applyPolicy } from '../wallet/policy.js'; +import { + newApproval, + submitApproval, + rejectMasterKey, + type Approval, +} from '../wallet/approval.js'; +import { hlInfo } from '../core/http.js'; +import { AgentCliError } from '../errors/index.js'; +register('init', { + description: 'Create agent and emit resumable master-wallet approvals', + schema: z.object({ + masterAddress: z + .string() + .regex(/^0x[0-9a-fA-F]{40}$/) + .optional(), + validUntil: z.number().int().optional(), + signature: z.string().optional(), + builderSignature: z.string().optional(), + qr: z.boolean().default(true), + }), + execute: async (input, context) => { + rejectMasterKey(input); + const config = await loadConfig(context.configPath); + if (context.network) config.network = context.network; + if (config.approved && !config.revoked) + return { ...config, expiry: config.agentExpiry }; + config.masterAddress = + (input.masterAddress as string) ?? config.masterAddress; + if (!config.masterAddress) + throw new AgentCliError('CONFIG_INVALID', 'Provide masterAddress'); + const expiry = + (input.validUntil as number) ?? + config.agentExpiry ?? + Date.now() + 179 * 86400000; + if (expiry <= Date.now() || expiry > Date.now() + 180 * 86400000) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Expiry must be within 180 days', + ); + config.agentExpiry = expiry; + const client = await privyClient(config); + const policyId = await applyPolicy(config, client, context.configPath); + await createAgent(config, client, context.configPath, policyId); + const path = join( + stateDir(), + `approval-${config.network}-${config.agentAddress}.json`, + ); + let pending: { agent: Approval; builder?: Approval }; + try { + pending = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + pending = { + agent: await newApproval(config, 'approveAgent', { + agentAddress: config.agentAddress, + agentName: `${config.agentName} valid_until ${expiry}`, + }), + }; + if (config.builder.address) + pending.builder = await newApproval(config, 'approveBuilderFee', { + builder: config.builder.address, + maxFeeRate: `${config.builder.fee / 1000}%`, + }); + await atomicWrite(path, JSON.stringify(pending)); + } + if (input.signature) + await submitApproval( + config.network, + pending.agent, + input.signature as string, + ); + if (input.builderSignature && pending.builder) + await submitApproval( + config.network, + pending.builder, + input.builderSignature as string, + ); + const agents = await hlInfo<{ address: string; validUntil: number }[]>( + config.network, + 'extraAgents', + { user: config.masterAddress }, + ); + config.approved = agents.some( + (agent) => + agent.address.toLowerCase() === config.agentAddress?.toLowerCase() && + agent.validUntil >= expiry, + ); + if (config.builder.address) { + const max = await hlInfo(config.network, 'maxBuilderFee', { + user: config.masterAddress, + builder: config.builder.address, + }); + config.approved = config.approved && max >= config.builder.fee; + } + config.revoked = false; + await saveConfig(config, context.configPath); + const deeplink = `ethereum:${config.masterAddress}?request=${encodeURIComponent(JSON.stringify(pending.agent.typedData))}`; + return { + masterAddress: config.masterAddress, + agentAddress: config.agentAddress, + expiry, + policyId, + approved: config.approved, + requiresSignature: !config.approved, + approvals: pending, + deeplink, + ...(await renderQr(deeplink, { enabled: input.qr as boolean })), + }; + }, +}); diff --git a/src/commands/leverage.ts b/src/commands/leverage.ts new file mode 100644 index 0000000..f4f8f09 --- /dev/null +++ b/src/commands/leverage.ts @@ -0,0 +1,60 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import type { Config } from '../core/config.js'; +import { + assertCanTrade, + tradingServices, + exchangeAction, + requiredDailyLoss, + type TradingServices, +} from '../core/trading.js'; +import { parseUsdc } from '../funding/amounts.js'; +import { AgentCliError } from '../errors/index.js'; +export const leverageSchema = z.object({ + coin: z.string(), + leverage: z.number().int().positive(), + mode: z.enum(['cross', 'isolated']).default('cross'), + intent: z.string().optional(), +}); +export async function setLeverage( + config: Config, + input: z.infer, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const market = await clients.market(input.coin); + if ( + (config.allowedMarkets.length && + !config.allowedMarkets.includes(input.coin)) || + input.leverage > + Math.min(market.maxLeverage, config.limits.maxLeverage ?? Infinity) + ) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Leverage or market exceeds configured limits', + ); + if ( + config.limits.dailyLoss !== undefined && + parseUsdc(await requiredDailyLoss(clients), 8) > + parseUsdc(String(config.limits.dailyLoss), 8) + ) + throw new AgentCliError('POLICY_REJECTED', 'Daily loss cap exceeded'); + return exchangeAction('updateLeverage', () => + clients.exchange.updateLeverage({ + asset: market.asset, + isCross: input.mode === 'cross', + leverage: input.leverage, + }), + ); +} +register('leverage', { + description: 'Set market leverage and cross/isolated margin mode', + schema: leverageSchema, + execute: async (input, context) => + setLeverage( + await commandConfig(context), + input as z.infer, + ), +}); diff --git a/src/commands/logs.ts b/src/commands/logs.ts new file mode 100644 index 0000000..a480f63 --- /dev/null +++ b/src/commands/logs.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { register } from '../core/router.js'; +import { stateDir } from '../core/config.js'; +import { levels, redact, type Level } from '../core/logger.js'; +export async function readLogs( + options: { + directory?: string; + level?: Level; + since?: string; + requestId?: string; + } = {}, +) { + const directory = options.directory ?? join(stateDir(), 'logs'); + let files: string[]; + try { + files = await readdir(directory); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } + const lines = ( + await Promise.all( + files + .filter((f) => /^runner.*\.jsonl$/.test(f)) + .map((f) => readFile(join(directory, f), 'utf8')), + ) + ).flatMap((content) => + content + .split('\n') + .slice(0, -1) + .filter(Boolean) + .map((line) => JSON.parse(line) as Record), + ); + return lines + .filter( + (line) => + (!options.level || + levels[line.level as Level] >= levels[options.level]) && + (!options.since || String(line.time) >= options.since) && + (!options.requestId || line.requestId === options.requestId), + ) + .sort((a, b) => String(a.time).localeCompare(String(b.time))) + .map(redact); +} +register('logs', { + description: 'Read/follow redacted JSON-line runner logs', + schema: z.object({ + follow: z.boolean().default(false), + level: z.enum(['debug', 'info', 'warn', 'error']).optional(), + since: z.string().datetime().optional(), + requestId: z.string().optional(), + }), + execute: async (input, context) => { + context.streaming = true; + let cursor = 0; + const seen = new Set(); + const abort = new AbortController(); + const stop = () => abort.abort(); + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + try { + do { + const logs = await readLogs(input); + for (const log of logs) { + const key = JSON.stringify(log); + if (!seen.has(key)) { + seen.add(key); + process.stdout.write(key + '\n'); + cursor++; + } + } + if (input.follow && !abort.signal.aborted) + await new Promise((resolve) => setTimeout(resolve, 250)); + } while (input.follow && !abort.signal.aborted); + } finally { + process.off('SIGINT', stop); + process.off('SIGTERM', stop); + } + return { lines: cursor }; + }, +}); diff --git a/src/commands/market.ts b/src/commands/market.ts new file mode 100644 index 0000000..f9f8291 --- /dev/null +++ b/src/commands/market.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { createClients } from '../core/hlclient.js'; +import { hlInfo } from '../core/http.js'; +import { commandConfig } from './read.js'; +import { AgentCliError } from '../errors/index.js'; +import { mapHyperliquidError } from '../errors/mappers.js'; +register('market', { + description: + 'Discover market asset, precision, leverage ceiling and current mid', + schema: z.object({ coin: z.string() }), + execute: async (input, context) => { + const config = await commandConfig(context); + try { + const [market, mids] = await Promise.all([ + createClients(config.network).meta.lookup(input.coin as string), + hlInfo>(config.network, 'allMids'), + ]); + const mid = mids[market.name]; + if (!mid) + throw new AgentCliError('UNKNOWN_MARKET', 'Market has no mid price'); + return { + coin: market.name, + asset: String(market.asset), + szDecimals: String(market.szDecimals), + maxLeverage: String(market.maxLeverage), + mid, + }; + } catch (error) { + throw mapHyperliquidError(error); + } + }, +}); diff --git a/src/commands/modify.ts b/src/commands/modify.ts new file mode 100644 index 0000000..b2f3df2 --- /dev/null +++ b/src/commands/modify.ts @@ -0,0 +1,114 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import type { Config } from '../core/config.js'; +import { + assertCanTrade, + tradingServices, + checkRisk, + exchangeAction, + type TradingServices, +} from '../core/trading.js'; +import { roundSize, roundPrice } from '../core/hlclient.js'; +import { AgentCliError } from '../errors/index.js'; +const modification = z.object({ + oid: z.number().int().positive().optional(), + cloid: z + .string() + .regex(/^0x[0-9a-fA-F]{32}$/) + .optional(), + size: z.string().optional(), + price: z.string().optional(), +}); +export const modifySchema = modification.extend({ + modifications: z.array(modification).optional(), + intent: z.string().optional(), +}); +export async function modifyOrders( + config: Config, + input: z.infer, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const orders = await clients.orders(); + const staged = [...orders]; + const stagedServices = { ...clients, orders: async () => staged }; + const requests = input.modifications ?? [input]; + const modifies: Record[] = []; + for (const request of requests) { + if (!request.oid && !request.cloid) + throw new AgentCliError('CONFIG_INVALID', 'Specify oid or cloid'); + const original = orders.find((o) => + request.oid ? o.oid === request.oid : o.cloid === request.cloid, + ); + if (!original) + throw new AgentCliError('ORDER_NOT_FOUND', 'Resting order absent'); + if ( + original.isTrigger && + (!original.triggerPx || + !/take profit|stop/i.test(original.orderType ?? '')) + ) + throw new AgentCliError( + 'UPSTREAM_ERROR', + 'Trigger order metadata incomplete', + ); + const market = await clients.market(original.coin); + const size = roundSize(request.size ?? original.sz, market.szDecimals); + const price = roundPrice( + request.price ?? original.limitPx, + market.szDecimals, + ); + const state = await clients.state(); + const leverage = + state.assetPositions.find((p) => p.position.coin === original.coin) + ?.position.leverage.value ?? 1; + await checkRisk( + config, + { + coin: original.coin, + size, + price, + side: original.side === 'B' ? 'buy' : 'sell', + leverage, + reduceOnly: original.reduceOnly, + }, + stagedServices, + original.oid, + ); + const index = staged.findIndex((order) => order.oid === original.oid); + staged[index] = { ...original, sz: size, limitPx: price }; + modifies.push({ + oid: request.cloid ?? original.oid, + order: { + a: market.asset, + b: original.side === 'B', + p: price, + s: size, + r: original.reduceOnly ?? false, + t: original.isTrigger + ? { + trigger: { + triggerPx: roundPrice(original.triggerPx!, market.szDecimals), + isMarket: /market/i.test(original.orderType!), + tpsl: /take profit/i.test(original.orderType!) ? 'tp' : 'sl', + }, + } + : { limit: { tif: original.tif ?? 'Gtc' } }, + ...(original.cloid ? { c: original.cloid } : {}), + }, + }); + } + return exchangeAction('batchModify', () => + clients.exchange.batchModify({ modifies }), + ); +} +register('modify', { + description: 'Modify price/size of single or batch resting orders', + schema: modifySchema, + execute: async (input, context) => + modifyOrders( + await commandConfig(context), + input as z.infer, + ), +}); diff --git a/src/commands/order.ts b/src/commands/order.ts new file mode 100644 index 0000000..c2652fe --- /dev/null +++ b/src/commands/order.ts @@ -0,0 +1,148 @@ +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { type Config } from '../core/config.js'; +import { roundSize, roundPrice } from '../core/hlclient.js'; +import { approvedBuilder, builderRate } from '../core/builder.js'; +import { + assertCanTrade, + tradingServices, + checkRisk, + exchangeAction, + type TradingServices, +} from '../core/trading.js'; +import { AgentCliError } from '../errors/index.js'; +export const orderSchema = z.object({ + coin: z.string(), + side: z.enum(['buy', 'sell']), + size: z.string(), + price: z.string().optional(), + type: z.enum(['limit', 'market', 'tp', 'sl']).default('limit'), + tif: z.enum(['Gtc', 'Ioc', 'Alo']).default('Gtc'), + reduceOnly: z.boolean().default(false), + cloid: z + .string() + .regex(/^0x[0-9a-fA-F]{32}$/) + .optional(), + triggerPrice: z.string().optional(), + slippage: z.number().min(0).max(0.1).default(0.01), + intent: z.string().optional(), +}); +export type OrderInput = z.infer; +export async function prepareOrder( + config: Config, + input: OrderInput, + services: TradingServices, +) { + const market = await services.market(input.coin); + const size = roundSize(input.size, market.szDecimals); + let price = input.price; + if ( + input.type === 'market' || + ((input.type === 'tp' || input.type === 'sl') && !price) + ) { + const mid = (await services.mids())[input.coin]; + if (!mid) throw new AgentCliError('UNKNOWN_MARKET', 'Missing market price'); + price = boundedMarketPrice( + mid, + input.slippage, + input.side, + market.szDecimals, + ); + } + if (!price) throw new AgentCliError('INVALID_PRICE', 'Provide limit price'); + price = roundPrice(price, market.szDecimals); + const state = await services.state(); + const leverage = + state.assetPositions.find((p) => p.position.coin === input.coin)?.position + .leverage.value ?? 1; + await checkRisk( + config, + { + coin: input.coin, + size, + price, + side: input.side, + leverage, + reduceOnly: input.reduceOnly, + }, + services, + ); + let type: unknown = { + limit: { tif: input.type === 'market' ? 'Ioc' : input.tif }, + }; + if (input.type === 'tp' || input.type === 'sl') { + if (!input.triggerPrice) + throw new AgentCliError('INVALID_PRICE', 'Trigger price required'); + type = { + trigger: { + isMarket: !input.price, + triggerPx: roundPrice(input.triggerPrice, market.szDecimals), + tpsl: input.type, + }, + }; + } + return { + a: market.asset, + b: input.side === 'buy', + p: price, + s: size, + r: input.reduceOnly, + t: type, + ...(input.cloid ? { c: input.cloid } : {}), + }; +} +export async function placeOrder( + config: Config, + input: OrderInput, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const order = await prepareOrder(config, input, clients); + const builder = await approvedBuilder(config); + const result = await exchangeAction('order', () => + clients.exchange.order({ + orders: [order], + grouping: 'na', + ...(builder ? { builder } : {}), + }), + ); + return { + result, + builderFeeRate: builderRate(config), + builderAddress: config.builder.address, + cloid: input.cloid ?? null, + }; +} +register('order', { + description: 'Place limit, IOC market or TP/SL order', + schema: orderSchema, + execute: async (input, context) => + placeOrder(await commandConfig(context), input as OrderInput), +}); + +export function boundedMarketPrice( + mid: string, + slippage: number, + side: 'buy' | 'sell', + szDecimals: number, +) { + const units = parseUsdc(mid, 18); + const ratio = BigInt(Math.round(slippage * 1e9)); + const bound = + (units * (side === 'buy' ? 1000000000n + ratio : 1000000000n - ratio)) / + 1000000000n; + const rounded = roundPrice(formatUsdc(bound, 18), szDecimals); + if (side === 'buy' || parseUsdc(rounded, 18) >= bound) return rounded; + const precision = Math.max( + 0, + Math.min( + 6 - szDecimals, + 4 - Math.floor(Math.log10(Number(formatUsdc(bound, 18)))), + ), + ); + const tick = 10n ** BigInt(18 - precision); + return formatUsdc(parseUsdc(rounded, 18) + tick, 18); +} diff --git a/src/commands/read.ts b/src/commands/read.ts new file mode 100644 index 0000000..5fc86e5 --- /dev/null +++ b/src/commands/read.ts @@ -0,0 +1,169 @@ +import { collateralBalance } from '../core/account.js'; +import { runnerHealth } from '../runner/health.js'; +import { events } from '../runner/events.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { z } from 'zod'; +import { register, type Context } from '../core/router.js'; +import { loadConfig, type Config } from '../core/config.js'; +import { hlInfo } from '../core/http.js'; +import { pollDeposits } from '../funding/status.js'; +import { parseUsdc } from '../funding/amounts.js'; +import { AgentCliError } from '../errors/index.js'; +import { mapHyperliquidError } from '../errors/mappers.js'; +export interface AccountState { + marginSummary: { + accountValue: string; + totalMarginUsed: string; + totalNtlPos: string; + }; + withdrawable: string; + assetPositions: { + position: { + coin: string; + szi: string; + entryPx: string | null; + unrealizedPnl: string; + liquidationPx: string | null; + leverage: { type: string; value: number }; + }; + }[]; +} +export interface OpenOrder { + coin: string; + oid: number; + cloid?: string; + sz: string; + limitPx: string; + side: 'A' | 'B'; + reduceOnly?: boolean; + isTrigger?: boolean; + triggerPx?: string; + triggerCondition?: string; + orderType?: string; + tif?: 'Gtc' | 'Ioc' | 'Alo'; +} +export function accountBalance(state: AccountState) { + return { + accountValue: state.marginSummary.accountValue, + accountValueUnits: parseUsdc( + state.marginSummary.accountValue, + 8, + ).toString(), + withdrawable: state.withdrawable, + marginUsed: state.marginSummary.totalMarginUsed, + }; +} +export function positions(state: AccountState) { + return state.assetPositions.map(({ position: p }) => ({ + coin: p.coin, + size: p.szi, + entryPrice: p.entryPx, + unrealizedPnl: p.unrealizedPnl, + liquidationPrice: p.liquidationPx, + leverage: String(p.leverage.value), + marginMode: p.leverage.type, + })); +} +export function expiryStatus(config: Config, now = Date.now()) { + const days = + config.agentExpiry === undefined + ? null + : Math.max(0, (config.agentExpiry - now) / 86400000); + return { + expiry: config.agentExpiry ?? null, + daysRemaining: days === null ? null : String(days), + warnings: + days === null + ? [] + : config.expiryWarningDays + .filter((threshold) => days <= threshold) + .map((threshold) => `Agent expires within ${threshold} days`), + }; +} +export async function readCommand(name: string, config: Config) { + if (!config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Initialize master address'); + try { + if (name === 'orders') + return ( + await hlInfo(config.network, 'frontendOpenOrders', { + user: config.masterAddress, + }) + ).map((order) => + Object.fromEntries( + Object.entries(order).map(([key, value]) => [ + key, + typeof value === 'number' ? String(value) : value, + ]), + ), + ); + const state = await hlInfo( + config.network, + 'clearinghouseState', + { user: config.masterAddress }, + ); + if (name === 'balance') return accountBalance(state); + if (name === 'positions') return positions(state); + const [agents, deposits, maxBuilderFee] = await Promise.all([ + hlInfo<{ address: string; validUntil: number }[]>( + config.network, + 'extraAgents', + { user: config.masterAddress }, + ), + pollDeposits( + config.network, + config.masterAddress, + collateralBalance(state), + { + emit: activeRunner + ? async (deposit) => { + await events.append('deposit', { + id: deposit.id, + state: deposit.state, + }); + } + : undefined, + }, + ), + config.builder.address + ? hlInfo(config.network, 'maxBuilderFee', { + user: config.masterAddress, + builder: config.builder.address, + }) + : Promise.resolve(0), + ]); + return { + balance: accountBalance(state), + positions: positions(state), + agent: { + address: config.agentAddress, + approved: agents.some( + (a) => a.address.toLowerCase() === config.agentAddress?.toLowerCase(), + ), + revoked: config.revoked, + ...expiryStatus(config), + }, + policy: { + id: config.policyId ?? null, + version: String(config.policyVersion ?? 0), + }, + deposits: deposits.map((d) => ({ id: d.id, state: d.state })), + approvedMaxBuilderFee: String(maxBuilderFee), + runner: await runnerHealth(), + }; + } catch (error) { + throw mapHyperliquidError(error); + } +} +export async function commandConfig(context: Context) { + const config = await loadConfig(context.configPath); + if (context.network) config.network = context.network; + return config; +} +for (const name of ['status', 'balance', 'positions', 'orders']) + register(name, { + description: `Read account ${name}`, + schema: z.object({}), + execute: async (_input, context) => + readCommand(name, await commandConfig(context)), + }); diff --git a/src/commands/revoke.ts b/src/commands/revoke.ts new file mode 100644 index 0000000..4eabdce --- /dev/null +++ b/src/commands/revoke.ts @@ -0,0 +1,74 @@ +import { z } from 'zod'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { cancelOrders, cancelSchema } from './cancel.js'; +import { + atomicWrite, + saveConfig, + stateDir, + type Config, +} from '../core/config.js'; +import { + newApproval, + submitApproval, + type Approval, +} from '../wallet/approval.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { AgentCliError } from '../errors/index.js'; +export function markRevoked(config: Config) { + config.revoked = true; + config.approved = false; + return config; +} +register('revoke', { + description: + 'Disable local agent and prepare master-signed exchange revocation', + schema: z.object({ + cancelOrders: z.boolean().default(false), + signature: z.string().optional(), + }), + execute: async (input, context) => { + const config = await commandConfig(context); + if (!config.agentAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'No agent to revoke'); + if (input.cancelOrders && !config.revoked) + await cancelOrders(config, cancelSchema.parse({ all: true })); + const path = join( + stateDir(), + `revoke-${config.network}-${config.agentAddress}.json`, + ); + let pending: Approval; + try { + pending = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + pending = await newApproval(config, 'approveAgent', { + agentAddress: '0x' + '0'.repeat(40), + agentName: config.agentName, + }); + await atomicWrite(path, JSON.stringify(pending)); + } + let exchangeRevoked = false; + if (input.signature) { + await submitApproval(config.network, pending, input.signature as string); + exchangeRevoked = true; + config.authorizationKeyRef = undefined; + config.privyWalletId = undefined; + } + markRevoked(config); + await saveConfig(config, context.configPath); + if (activeRunner) { + activeRunner.signer = undefined; + Object.assign(activeRunner.config, config); + } + return { + agentAddress: config.agentAddress, + locallyRevoked: true, + exchangeRevoked, + requiresSignature: !exchangeRevoked, + approval: pending, + }; + }, +}); diff --git a/src/commands/rotate.ts b/src/commands/rotate.ts new file mode 100644 index 0000000..efbcdc2 --- /dev/null +++ b/src/commands/rotate.ts @@ -0,0 +1,171 @@ +import { z } from 'zod'; +import { join } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { + atomicWrite, + saveConfig, + stateDir, + type Config, +} from '../core/config.js'; +import { privyClient, createAgent, privySigner } from '../wallet/privy.js'; +import { + newApproval, + submitApproval, + type Approval, +} from '../wallet/approval.js'; +import { hlInfo } from '../core/http.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { AgentCliError } from '../errors/index.js'; +import { events } from '../runner/events.js'; +export interface Rotation { + old: Config; + next: Config; + approval: Approval; + revoke: Approval; + swapped: boolean; + revoked: boolean; +} +export async function rotationHandoff( + rotation: Rotation, + dependencies: { + verify: () => Promise; + swap: (next: Config) => Promise; + persist: () => Promise; + }, +) { + if (!(await dependencies.verify())) + throw new AgentCliError( + 'AGENT_NOT_APPROVED', + 'New agent approval not visible; old agent remains active', + ); + if (!rotation.swapped) { + await dependencies.swap(rotation.next); + rotation.swapped = true; + await dependencies.persist(); + } + return rotation.revoke; +} +register('rotate', { + description: 'Approve new agent, hot-swap signer, then revoke old agent', + schema: z.object({ + signature: z.string().optional(), + revokeSignature: z.string().optional(), + }), + execute: async (input, context) => { + const current = await commandConfig(context); + if (!current.masterAddress || !current.agentAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Initialize first'); + const path = join( + stateDir(), + `rotation-${current.network}-${current.masterAddress}.json`, + ); + let rotation: Rotation; + const client = await privyClient(current); + try { + rotation = JSON.parse(await readFile(path, 'utf8')); + if (rotation.revoked && !input.signature && !input.revokeSignature) { + await atomicWrite( + `${path}.${Date.now()}.previous`, + JSON.stringify(rotation), + ); + throw Object.assign(new Error('Begin next rotation'), { + code: 'ENOENT', + }); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + const next: Config = { + ...structuredClone(current), + privyWalletId: undefined, + agentAddress: undefined, + authorizationKeyRef: undefined, + agentExpiry: Date.now() + 179 * 86400000, + approved: false, + revoked: false, + }; + await createAgent( + next, + client, + join(stateDir(), 'rotation-next.json'), + current.policyId, + ); + next.agentName = `blox-${next.agentAddress!.slice(-8)}`; + rotation = { + old: current, + next, + approval: await newApproval(current, 'approveAgent', { + agentAddress: next.agentAddress, + agentName: `${next.agentName} valid_until ${next.agentExpiry}`, + }), + revoke: await newApproval(current, 'approveAgent', { + agentAddress: '0x' + '0'.repeat(40), + agentName: current.agentName, + }), + swapped: false, + revoked: false, + }; + await atomicWrite(path, JSON.stringify(rotation)); + } + if (input.signature) { + await submitApproval( + current.network, + rotation.approval, + input.signature as string, + ); + await rotationHandoff(rotation, { + verify: async () => { + const agents = await hlInfo< + { address: string; validUntil: number }[] + >(current.network, 'extraAgents', { user: current.masterAddress }); + return agents.some( + (a) => + a.address.toLowerCase() === + rotation.next.agentAddress?.toLowerCase() && + a.validUntil >= rotation.next.agentExpiry!, + ); + }, + swap: async (next) => { + const signer = await privySigner(next, client); + next.approved = true; + await saveConfig(next, context.configPath); + if (activeRunner) { + activeRunner.signer = signer; + Object.assign(activeRunner.config, next); + } + await events.append('runner_state', { + rotation: { + old: rotation.old.agentAddress, + next: next.agentAddress, + }, + }); + }, + persist: () => atomicWrite(path, JSON.stringify(rotation)), + }); + } + if (input.revokeSignature) { + if (!rotation.swapped) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Approve and swap new agent before revoking old', + ); + await submitApproval( + current.network, + rotation.revoke, + input.revokeSignature as string, + ); + rotation.revoked = true; + await atomicWrite(path, JSON.stringify(rotation)); + } + return { + oldAgent: rotation.old.agentAddress, + newAgent: rotation.next.agentAddress, + swapped: rotation.swapped, + oldRevoked: rotation.revoked, + requiresSignature: !rotation.swapped, + approval: rotation.approval, + ...(rotation.swapped ? { revokeApproval: rotation.revoke } : {}), + }; + }, +}); diff --git a/src/commands/run.ts b/src/commands/run.ts new file mode 100644 index 0000000..5ca9ca8 --- /dev/null +++ b/src/commands/run.ts @@ -0,0 +1,48 @@ +import { startHeartbeat } from '../runner/health.js'; +import { events, eventTypes, type EventType } from '../runner/events.js'; +import { startSubscriptions } from '../runner/subscriptions.js'; +import { rpcServer } from '../runner/rpc.js'; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { Runner, detachRunner, setActiveRunner } from '../runner/lifecycle.js'; +register('run', { + description: 'Start persistent signing runner (foreground or detach)', + schema: z.object({ + detach: z.boolean().default(false), + daemon: z.boolean().default(false), + }), + execute: async (input, context) => { + if (input.detach || input.daemon) + return detachRunner([ + ...(context.configPath ? ['--config', context.configPath] : []), + ...(context.network ? ['--network', context.network] : []), + ]); + const runner = new Runner(await commandConfig(context)); + const info = await runner.start(); + if (!info.existing) { + setActiveRunner(runner); + runner.signals(); + runner.onShutdown(() => events.flush()); + try { + await rpcServer(runner, context); + const subscriptions = await startSubscriptions( + runner, + async (type, data) => { + if (eventTypes.includes(type as EventType)) + await events.append(type as EventType, data); + runner.logger.log(type === 'error' ? 'error' : 'info', type, { + data, + }); + }, + ); + startHeartbeat(runner, subscriptions); + await runner.markReady(); + } catch (error) { + await runner.shutdown(); + throw error; + } + } + return { ...info, ready: info.existing ? info.ready : runner.info.ready }; + }, +}); diff --git a/src/commands/skill.ts b/src/commands/skill.ts new file mode 100644 index 0000000..f372972 --- /dev/null +++ b/src/commands/skill.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { register } from '../core/router.js'; +import { atomicWrite } from '../core/config.js'; +import { cliVersion, skillContent } from '../core/skill-content.js'; +import { AgentCliError } from '../errors/index.js'; +export async function installSkill( + target: 'claude' | 'codex', + options: { home?: string; codexHome?: string; force?: boolean } = {}, +) { + const home = options.home ?? homedir(); + const root = + target === 'codex' + ? (options.codexHome ?? process.env.CODEX_HOME ?? join(home, '.codex')) + : join(home, '.claude'); + const path = join(root, 'skills', 'agent-cli', 'SKILL.md'); + try { + const existing = await readFile(path, 'utf8'); + if (existing === skillContent) + return { target, path, version: cliVersion, changed: false }; + if (!options.force) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Skill already exists with different content; use --force to replace', + ); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + await atomicWrite(path, skillContent); + return { target, path, version: cliVersion, changed: true }; +} +register('skill', { + description: + 'Install packaged version-matched skill for Claude Code or Codex', + schema: z.object({ + args: z.array(z.string()).default(['install']), + target: z.enum(['claude', 'codex']).default('codex'), + force: z.boolean().default(false), + }), + execute: async (input) => { + if ((input.args as string[])[0] !== 'install') + throw new AgentCliError( + 'CONFIG_INVALID', + 'Use skill install --target claude|codex', + ); + return installSkill(input.target as 'claude' | 'codex', { + force: input.force as boolean, + }); + }, +}); diff --git a/src/core/account.ts b/src/core/account.ts new file mode 100644 index 0000000..689e731 --- /dev/null +++ b/src/core/account.ts @@ -0,0 +1,13 @@ +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +// Remove marked PnL so a price move cannot masquerade as receipt of a deposit. +export function collateralBalance(state: { + marginSummary: { accountValue: string }; + assetPositions?: { position: { unrealizedPnl: string } }[]; +}) { + const equity = parseUsdc(state.marginSummary.accountValue, 8); + const pnl = (state.assetPositions ?? []).reduce( + (sum, { position }) => sum + parseUsdc(position.unrealizedPnl, 8), + 0n, + ); + return formatUsdc(equity - pnl, 8); +} diff --git a/src/core/audit.ts b/src/core/audit.ts new file mode 100644 index 0000000..aa516c6 --- /dev/null +++ b/src/core/audit.ts @@ -0,0 +1,163 @@ +import { requestContext as context } from './request-context.js'; +import { createHash, randomUUID } from 'node:crypto'; +import { readFile, open, unlink, mkdir, chmod } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { stateDir } from './config.js'; +import { redact } from './logger.js'; +import { AgentCliError, normalizeError } from '../errors/index.js'; +export interface AuditEntry { + seq: number; + timestamp: string; + intentId: string; + cloid?: string; + requestId?: string; + stage: string; + data: unknown; + previousHash: string; + hash: string; +} +const digest = (value: unknown) => + createHash('sha256').update(JSON.stringify(value)).digest('hex'); + +export async function readAudit( + path = join(stateDir(), 'audit.jsonl'), +): Promise { + try { + return (await readFile(path, 'utf8')) + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } +} +export function verifyAudit(entries: AuditEntry[]) { + let previousHash = '0'.repeat(64); + let seq = 0; + for (const entry of entries) { + const { hash, ...payload } = entry; + if ( + entry.seq !== ++seq || + entry.previousHash !== previousHash || + digest(payload) !== hash + ) + throw new AgentCliError('INTERNAL', 'Audit chain integrity failure'); + previousHash = hash; + } + return true; +} +export class AuditStore { + private queue = Promise.resolve(); + constructor(readonly path = join(stateDir(), 'audit.jsonl')) {} + append( + stage: string, + data: unknown, + metadata = context.getStore() ?? { intentId: randomUUID() }, + ) { + const operation = this.queue.then(async () => { + await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }); + await chmod(dirname(this.path), 0o700); + const lock = this.path + '.lock'; + let handle; + for (let i = 0; i < 100; i++) { + try { + handle = await open(lock, 'wx', 0o600); + break; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + if (!handle) + throw new AgentCliError( + 'INTERNAL', + 'Audit lock busy; inspect stale lock', + ); + try { + const entries = await readAudit(this.path); + verifyAudit(entries); + const last = entries.at(-1); + const payload = { + seq: (last?.seq ?? 0) + 1, + timestamp: new Date().toISOString(), + ...metadata, + stage, + data: redact(data), + previousHash: last?.hash ?? '0'.repeat(64), + }; + const entry = { ...payload, hash: digest(payload) }; + const file = await open(this.path, 'a', 0o600); + await file.chmod(0o600); + try { + await file.writeFile(JSON.stringify(entry) + '\n'); + await file.sync(); + } finally { + await file.close(); + } + return entry; + } finally { + await handle.close(); + await unlink(lock); + } + }); + this.queue = operation.then(() => {}); + return operation; + } + async flush() { + await this.queue; + } +} +export const audit = new AuditStore(); +export async function auditedAction( + name: string, + input: Record, + operation: () => Promise, +): Promise { + return context.run( + { + intentId: (input.intentId as string) ?? randomUUID(), + cloid: input.cloid as string | undefined, + requestId: randomUUID(), + }, + async () => { + await audit.append('intent', { action: name, input }); + try { + const result = await operation(); + await audit.append('response', { action: name, result }); + return result; + } catch (error) { + await audit.append('decision', { + allowed: false, + error: normalizeError(error).toJSON(), + }); + throw error; + } + }, + ); +} +export async function signatureAudit(signature: string) { + if (context.getStore()) + await audit.append('signature', { + digest: createHash('sha256').update(signature).digest('base64'), + }); +} +export async function decisionAudit(data: unknown) { + if (context.getStore()) await audit.append('decision', data); +} +export async function auditFill(fill: Record) { + const entries = await readAudit(); + const linked = [...entries] + .reverse() + .find( + (entry) => + (entry.cloid && entry.cloid === fill.cloid) || + (entry.stage === 'response' && + fill.oid !== undefined && + JSON.stringify(entry.data).includes(`"oid":${fill.oid}`)), + ); + await audit.append('fill', fill, { + intentId: linked?.intentId ?? 'external', + cloid: linked?.cloid, + }); +} diff --git a/src/core/builder.ts b/src/core/builder.ts new file mode 100644 index 0000000..adb77e4 --- /dev/null +++ b/src/core/builder.ts @@ -0,0 +1,31 @@ +import type { Config } from './config.js'; +import { hlInfo } from './http.js'; +import { AgentCliError } from '../errors/index.js'; +export async function approvedBuilder( + config: Config, + fetchMaximum = (user: string, builder: string) => + hlInfo(config.network, 'maxBuilderFee', { user, builder }), +) { + if (!config.builder.address) { + if (config.builder.fee) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Builder address is required for a fee', + ); + return undefined; + } + if (!config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Master address missing'); + const maximum = await fetchMaximum( + config.masterAddress, + config.builder.address, + ); + if (config.builder.fee > maximum) + throw new AgentCliError( + 'AGENT_NOT_APPROVED', + 'Approve configured builder fee with master wallet', + { approvedMax: maximum, requested: config.builder.fee }, + ); + return { b: config.builder.address.toLowerCase(), f: config.builder.fee }; +} +export const builderRate = (config: Config) => `${config.builder.fee / 1000}%`; diff --git a/src/core/config.ts b/src/core/config.ts new file mode 100644 index 0000000..5bc574b --- /dev/null +++ b/src/core/config.ts @@ -0,0 +1,109 @@ +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { mkdir, readFile, rename, writeFile, chmod } from 'node:fs/promises'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { AgentCliError } from '../errors/index.js'; +export const stateDir = () => join(homedir(), '.bloxwap'); +const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/); +const positive = z.number().positive().finite(); +export const configSchema = z + .object({ + version: z.literal(1).default(1), + network: z.enum(['mainnet', 'testnet']).default('testnet'), + agentName: z.string().max(16).default('bloxwap'), + masterAddress: address.optional(), + agentAddress: address.optional(), + agentExpiry: z.number().int().positive().optional(), + privyAppId: z.string().optional(), + privyWalletId: z.string().optional(), + policyId: z.string().optional(), + policyVersion: z.number().int().optional(), + authorizationKeyRef: z + .string() + .regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/) + .optional(), + privyAppSecretRef: z + .string() + .regex(/^env:[A-Z_][A-Z0-9_]*$/) + .optional(), + revoked: z.boolean().default(false), + approved: z.boolean().default(false), + limits: z + .object({ + maxNotional: positive.optional(), + maxPositionNotional: positive.optional(), + maxLeverage: positive.optional(), + maxOrderSize: positive.optional(), + dailyLoss: positive.optional(), + maxOpenOrders: z.number().int().nonnegative().optional(), + }) + .default({}), + allowedMarkets: z.array(z.string()).default([]), + builder: z + .object({ + address: address.default('0x71b09A08257078A4d642F7Dd7315e656c837329a'), + fee: z.number().int().min(0).max(100).default(10), + }) + .default({ + address: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + fee: 10, + }), + logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + runnerFallback: z.boolean().default(true), + expiryWarningDays: z.array(positive).default([14, 3, 1]), + }) + .strict(); +export type Config = z.infer; +export const configPath = () => join(stateDir(), 'config.json'); +export async function atomicWrite(path: string, data: string) { + await mkdir(dirname(path), { recursive: true, mode: 0o700 }); + await chmod(dirname(path), 0o700); + const temporary = `${path}.${randomUUID()}.tmp`; + await writeFile(temporary, data, { mode: 0o600, flag: 'wx' }); + await rename(temporary, path); + await chmod(path, 0o600); +} +export function migrate(raw: unknown): Config { + if (raw && typeof raw === 'object' && !('version' in raw)) + raw = { ...raw, version: 1 }; + const result = configSchema.safeParse(raw); + if (!result.success) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Invalid config', + result.error.flatten(), + ); + return result.data; +} +export async function loadConfig( + path = configPath(), + env: NodeJS.ProcessEnv = process.env, +): Promise { + let raw: unknown = {}; + try { + raw = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') + throw new AgentCliError('CONFIG_INVALID', 'Cannot read config'); + } + const config = migrate(raw); + for (const [name, key] of Object.entries({ + BLOXWAP_NETWORK: 'network', + BLOXWAP_LOG_LEVEL: 'logLevel', + BLOXWAP_PRIVY_APP_ID: 'privyAppId', + BLOXWAP_MASTER_ADDRESS: 'masterAddress', + })) { + if (env[name]) Object.assign(config, { [key]: env[name] }); + } + if (env.BLOXWAP_BUILDER_ADDRESS !== undefined) + config.builder.address = env.BLOXWAP_BUILDER_ADDRESS; + if (env.BLOXWAP_BUILDER_FEE !== undefined) + config.builder.fee = env.BLOXWAP_BUILDER_FEE.trim() + ? Number(env.BLOXWAP_BUILDER_FEE) + : NaN; + return migrate(config); +} +export async function saveConfig(config: unknown, path = configPath()) { + await atomicWrite(path, JSON.stringify(migrate(config), null, 2) + '\n'); +} diff --git a/src/core/hlclient.ts b/src/core/hlclient.ts new file mode 100644 index 0000000..0279274 --- /dev/null +++ b/src/core/hlclient.ts @@ -0,0 +1,127 @@ +import { + ExchangeClient, + HttpTransport, + InfoClient, + SubscriptionClient, + WebSocketTransport, +} from '@bloxwap/hyperliquid'; +import type { AbstractViemLocalAccount } from '@bloxwap/hyperliquid/signing'; +import { formatPrice, formatSize } from '@bloxwap/hyperliquid/utils'; +import { AgentCliError } from '../errors/index.js'; +export type Signer = AbstractViemLocalAccount; +export interface Market { + name: string; + szDecimals: number; + maxLeverage: number; + asset: number; +} +export class MetaCache { + private markets = new Map(); + private updated = 0; + constructor( + private fetchMeta: () => Promise<{ + universe: { name: string; szDecimals: number; maxLeverage: number }[]; + }>, + private ttl = 60_000, + ) {} + async refresh() { + const meta = await this.fetchMeta(); + if ( + !Array.isArray(meta.universe) || + meta.universe.some( + (market) => + !market.name || + !Number.isInteger(market.szDecimals) || + market.szDecimals < 0 || + market.szDecimals > 6 || + !Number.isInteger(market.maxLeverage) || + market.maxLeverage < 1, + ) + ) + throw new AgentCliError('UPSTREAM_ERROR', 'Invalid market metadata'); + this.markets = new Map( + meta.universe.map((market, asset) => [market.name, { ...market, asset }]), + ); + this.updated = Date.now(); + } + async lookup(coin: string) { + if (Date.now() - this.updated > this.ttl) await this.refresh(); + const market = this.markets.get(coin); + if (!market) + throw new AgentCliError('UNKNOWN_MARKET', `Unknown market ${coin}`); + return market; + } +} +export function roundSize(size: string, szDecimals: number) { + if (!/^\d+(\.\d+)?$/.test(size) || Number(size) <= 0) + throw new AgentCliError('INVALID_SIZE', 'Size must be positive'); + const rounded = formatSize(size, szDecimals); + if (Number(rounded) <= 0) + throw new AgentCliError('INVALID_SIZE', 'Size rounds to zero'); + return rounded; +} +export function roundPrice(price: string, szDecimals: number) { + if (!/^\d+(\.\d+)?$/.test(price) || Number(price) <= 0) + throw new AgentCliError('INVALID_PRICE', 'Price must be positive'); + const rounded = formatPrice(price, szDecimals); + if (Number(rounded) <= 0) + throw new AgentCliError('INVALID_PRICE', 'Price rounds to zero'); + return rounded; +} +// Bound SDK generic depth at the adapter boundary; runtime clients remain unchanged. +const Exchange = ExchangeClient as unknown as new (options: { + transport: HttpTransport; + wallet: Signer; + nonceManager?: () => Promise; +}) => ExchangeAdapter; +export interface ExchangeAdapter { + order(params: Record): Promise; + cancel(params: Record): Promise; + cancelByCloid(params: Record): Promise; + batchModify(params: Record): Promise; + updateLeverage(params: Record): Promise; +} +const readClients = new Map< + 'mainnet' | 'testnet', + { transport: HttpTransport; info: InfoClient; meta: MetaCache } +>(); +export function createClients( + network: 'mainnet' | 'testnet', + signer?: Signer, + nonce?: () => Promise, +) { + let reads = readClients.get(network); + if (!reads) { + const transport = new HttpTransport({ isTestnet: network === 'testnet' }); + const info = new InfoClient({ transport }); + const meta = new MetaCache( + ( + info as unknown as { + meta: () => Promise<{ + universe: { + name: string; + szDecimals: number; + maxLeverage: number; + }[]; + }>; + } + ).meta.bind(info), + ); + reads = { transport, info, meta }; + readClients.set(network, reads); + } + const { transport, info, meta } = reads; + return { + info, + meta, + exchange: signer + ? new Exchange({ transport, wallet: signer, nonceManager: nonce }) + : undefined, + subscriptions: () => { + const transport = new WebSocketTransport({ + isTestnet: network === 'testnet', + }); + return { transport, client: new SubscriptionClient({ transport }) }; + }, + }; +} diff --git a/src/core/http.ts b/src/core/http.ts new file mode 100644 index 0000000..53604eb --- /dev/null +++ b/src/core/http.ts @@ -0,0 +1,37 @@ +import { mapNetworkError } from '../errors/mappers.js'; +export async function jsonRequest( + url: string, + body?: unknown, + fetcher: typeof fetch = fetch, +) { + let response: Response; + try { + response = await fetcher(url, { + method: body === undefined ? 'GET' : 'POST', + headers: { 'content-type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + signal: AbortSignal.timeout(15_000), + }); + } catch (error) { + throw mapNetworkError(error); + } + const data: unknown = await response.json(); + if (!response.ok) + throw { + status: response.status, + ...(data && typeof data === 'object' ? data : { message: String(data) }), + }; + return data; +} +export const hlEndpoint = (network: 'mainnet' | 'testnet') => + `https://api${network === 'testnet' ? '.hyperliquid-testnet' : '.hyperliquid'}.xyz`; +export async function hlInfo( + network: 'mainnet' | 'testnet', + type: string, + params: Record = {}, +): Promise { + return (await jsonRequest(`${hlEndpoint(network)}/info`, { + type, + ...params, + })) as T; +} diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 0000000..bebe36f --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,104 @@ +import { currentRequestContext } from './request-context.js'; +import { randomUUID } from 'node:crypto'; +import { appendFile, mkdir, stat, rename, chmod } from 'node:fs/promises'; +import { join } from 'node:path'; +import { stateDir } from './config.js'; +export const levels = { debug: 10, info: 20, warn: 30, error: 40 }; +export type Level = keyof typeof levels; +export function redact(value: unknown): unknown { + if (typeof value === 'string') { + let safe = value; + for (const [key, secret] of Object.entries(process.env)) + if ( + /secret|token|authorization|private.?key|password|api.?key|auth.?key|app.?key/i.test( + key, + ) && + secret && + secret.length >= 4 + ) + safe = safe.split(secret).join('[REDACTED]'); + return safe + .replace( + /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g, + '[REDACTED]', + ) + .replace(/[A-Za-z0-9+/]{80,}={0,2}/g, '[REDACTED]') + .replace(/(?:0x)?[a-fA-F0-9]{64,}/g, '[REDACTED]') + .replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]'); + } + + if (Array.isArray(value)) return value.map(redact); + if (value && typeof value === 'object') + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + /secret|signature|token|authorization|private.?key|seed|mnemonic|password|app.?key|api.?key|auth.?key/i.test( + key, + ) + ? '[REDACTED]' + : redact(v), + ]), + ); + return value; +} +export class Logger { + private queue: Promise = Promise.resolve(); + private day = new Date().toISOString().slice(0, 10); + readonly requestId: string; + constructor( + private options: { + level?: Level; + directory?: string; + maxBytes?: number; + requestId?: string; + intentId?: string; + stderr?: (line: string) => void; + } = {}, + ) { + this.requestId = + options.requestId ?? currentRequestContext()?.requestId ?? randomUUID(); + } + log(level: Level, message: string, fields: Record = {}) { + if (levels[level] < levels[this.options.level ?? 'info']) return; + const line = + JSON.stringify( + redact({ + ...fields, + time: new Date().toISOString(), + level, + message, + requestId: this.requestId, + intentId: this.options.intentId ?? currentRequestContext()?.intentId, + }), + ) + '\n'; + (this.options.stderr ?? ((line) => process.stderr.write(line)))(line); + this.queue = this.queue.then(async () => { + const dir = this.options.directory ?? join(stateDir(), 'logs'); + await mkdir(dir, { recursive: true, mode: 0o700 }); + await chmod(dir, 0o700); + const path = join(dir, 'runner.jsonl'); + const day = new Date().toISOString().slice(0, 10); + try { + const info = await stat(path); + if ( + info.size + Buffer.byteLength(line) > + (this.options.maxBytes ?? 5_000_000) || + day !== this.day || + info.mtime.toISOString().slice(0, 10) !== day + ) + await rename( + path, + join(dir, `runner.${Date.now()}.${randomUUID()}.jsonl`), + ); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + this.day = day; + await appendFile(path, line, { mode: 0o600 }); + await chmod(path, 0o600); + }); + } + async flush() { + await this.queue; + } +} diff --git a/src/core/nonce.ts b/src/core/nonce.ts new file mode 100644 index 0000000..2b04d94 --- /dev/null +++ b/src/core/nonce.ts @@ -0,0 +1,47 @@ +import { mkdir, readFile, open, unlink } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { atomicWrite, stateDir } from './config.js'; +import { AgentCliError } from '../errors/index.js'; +export async function nextNonce( + signer: string, + directory = join(stateDir(), 'nonces'), + now = Date.now, +): Promise { + if (!/^0x[0-9a-fA-F]{40}$/.test(signer)) + throw new AgentCliError('CONFIG_INVALID', 'Invalid nonce signer'); + const path = join(directory, `${signer.toLowerCase()}.json`); + const lock = `${path}.lock`; + await mkdir(dirname(path), { recursive: true, mode: 0o700 }); + for (let attempt = 0; attempt < 100; attempt++) { + let handle; + try { + handle = await open(lock, 'wx', 0o600); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; + await new Promise((resolve) => setTimeout(resolve, 10)); + continue; + } + try { + await handle.writeFile(String(process.pid)); + let previous = 0; + try { + previous = JSON.parse(await readFile(path, 'utf8')) as number; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') + throw new AgentCliError('CONFIG_INVALID', 'Corrupt nonce state'); + } + if (!Number.isSafeInteger(previous) || previous < 0) + throw new AgentCliError('CONFIG_INVALID', 'Invalid nonce state'); + const nonce = Math.max(now(), previous + 1); + await atomicWrite(path, JSON.stringify(nonce)); + return nonce; + } finally { + await handle.close(); + await unlink(lock); + } + } + throw new AgentCliError( + 'NONCE_COLLISION', + 'Nonce lock busy; inspect stale lock before retrying', + ); +} diff --git a/src/core/output-schemas.ts b/src/core/output-schemas.ts new file mode 100644 index 0000000..0f06a18 --- /dev/null +++ b/src/core/output-schemas.ts @@ -0,0 +1,250 @@ +// Stable command data shapes; unknown upstream response fields remain extensible. +const string = { type: 'string' }; +const boolean = { type: 'boolean' }; +const unknown = {}; +const object = ( + properties: Record, + required: string[] = [], +) => ({ type: 'object', properties, required }); +const array = (items: unknown) => ({ type: 'array', items }); +const balance = object( + { + accountValue: string, + accountValueUnits: string, + withdrawable: string, + marginUsed: string, + }, + ['accountValue', 'accountValueUnits', 'withdrawable', 'marginUsed'], +); +const positions = array( + object( + { + coin: string, + size: string, + entryPrice: { type: ['string', 'null'] }, + unrealizedPnl: string, + liquidationPrice: { type: ['string', 'null'] }, + leverage: string, + marginMode: string, + }, + ['coin', 'size', 'leverage'], + ), +); +const event = object( + { + seq: { type: 'integer', minimum: 1 }, + timestamp: { type: 'string', format: 'date-time' }, + type: { + enum: [ + 'fill', + 'order_update', + 'position_change', + 'deposit', + 'error', + 'agent_expiry_warning', + 'runner_state', + ], + }, + data: unknown, + }, + ['seq', 'timestamp', 'type', 'data'], +); +export const outputSchemas: Record = { + market: object( + { + coin: string, + asset: string, + szDecimals: string, + maxLeverage: string, + mid: string, + }, + ['coin', 'asset', 'szDecimals', 'maxLeverage', 'mid'], + ), + config: unknown, + init: object( + { + masterAddress: string, + agentAddress: string, + expiry: { type: 'number' }, + policyId: string, + approved: boolean, + requiresSignature: boolean, + approvals: unknown, + deeplink: string, + qrAscii: { type: ['string', 'null'] }, + }, + ['masterAddress', 'agentAddress', 'expiry', 'policyId', 'approved'], + ), + fund: object( + { + depositId: string, + depositAddress: string, + originChain: { type: 'number' }, + originCurrency: string, + minDeposit: string, + expectedReceived: string, + expectedReceivedHuman: string, + fees: unknown, + builderFeeRate: string, + builderAddress: string, + expiresAt: { type: 'number' }, + state: string, + signatureSteps: array(unknown), + qrAscii: { type: ['string', 'null'] }, + qrPath: { type: ['string', 'null'] }, + }, + [ + 'depositId', + 'depositAddress', + 'expectedReceived', + 'fees', + 'builderFeeRate', + 'builderAddress', + 'expiresAt', + 'qrAscii', + ], + ), + deposit: object({ + depositId: string, + steps: array(unknown), + complete: boolean, + }), + balance, + positions, + orders: array( + object( + { + coin: string, + oid: string, + sz: string, + limitPx: string, + side: { enum: ['A', 'B'] }, + cloid: string, + }, + ['coin', 'oid', 'sz', 'limitPx', 'side'], + ), + ), + status: object( + { + balance, + positions, + agent: object({ + address: string, + approved: boolean, + revoked: boolean, + expiry: { type: ['number', 'null'] }, + daysRemaining: { type: ['string', 'null'] }, + warnings: array(string), + }), + policy: object({ id: { type: ['string', 'null'] }, version: string }), + deposits: array( + object({ + id: string, + state: { + enum: [ + 'awaiting_funds', + 'pending', + 'settled', + 'failed', + 'refunded', + ], + }, + }), + ), + approvedMaxBuilderFee: string, + runner: { type: ['object', 'null'] }, + }, + [ + 'balance', + 'positions', + 'agent', + 'policy', + 'deposits', + 'approvedMaxBuilderFee', + 'runner', + ], + ), + order: object( + { + result: unknown, + builderFeeRate: string, + builderAddress: string, + cloid: { type: ['string', 'null'] }, + }, + ['result', 'builderFeeRate', 'builderAddress', 'cloid'], + ), + cancel: object({ cancelled: array(string), result: unknown }, [ + 'cancelled', + 'result', + ]), + modify: object({ status: string, response: unknown }, ['status']), + leverage: object({ status: string, response: unknown }, ['status']), + run: object( + { + pid: { type: 'integer' }, + startedAt: { type: 'number' }, + network: { enum: ['mainnet', 'testnet'] }, + ready: boolean, + existing: boolean, + }, + ['pid', 'startedAt', 'network'], + ), + events: array(event), + logs: { + description: 'JSON-line stream of redacted log objects', + type: 'object', + }, + rotate: object( + { + oldAgent: string, + newAgent: string, + swapped: boolean, + oldRevoked: boolean, + requiresSignature: boolean, + approval: unknown, + revokeApproval: unknown, + }, + ['oldAgent', 'newAgent', 'swapped', 'oldRevoked'], + ), + revoke: object( + { + agentAddress: string, + locallyRevoked: boolean, + exchangeRevoked: boolean, + requiresSignature: boolean, + approval: unknown, + }, + [ + 'agentAddress', + 'locallyRevoked', + 'exchangeRevoked', + 'requiresSignature', + 'approval', + ], + ), + audit: array( + object( + { + seq: { type: 'integer' }, + timestamp: string, + intentId: string, + requestId: string, + cloid: string, + stage: string, + data: unknown, + previousHash: string, + hash: string, + }, + ['seq', 'timestamp', 'intentId', 'stage', 'data', 'previousHash', 'hash'], + ), + ), + skill: object( + { + target: { enum: ['claude', 'codex'] }, + path: string, + version: string, + changed: boolean, + }, + ['target', 'path', 'version', 'changed'], + ), +}; diff --git a/src/core/request-context.ts b/src/core/request-context.ts new file mode 100644 index 0000000..471f054 --- /dev/null +++ b/src/core/request-context.ts @@ -0,0 +1,7 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +export const requestContext = new AsyncLocalStorage<{ + intentId: string; + cloid?: string; + requestId?: string; +}>(); +export const currentRequestContext = () => requestContext.getStore(); diff --git a/src/core/retry.ts b/src/core/retry.ts new file mode 100644 index 0000000..91c4b98 --- /dev/null +++ b/src/core/retry.ts @@ -0,0 +1,32 @@ +import { normalizeError } from '../errors/index.js'; +export async function retry( + operation: () => Promise, + options: { + attempts?: number; + baseMs?: number; + maxMs?: number; + random?: () => number; + sleep?: (ms: number) => Promise; + } = {}, +) { + const attempts = options.attempts ?? 3; + if (!Number.isInteger(attempts) || attempts < 1) + throw new RangeError('attempts must be positive'); + for (let attempt = 0; ; attempt++) { + try { + return await operation(); + } catch (error) { + const normalized = normalizeError(error); + if (!normalized.retryable || attempt + 1 >= attempts) throw normalized; + const base = Math.min( + options.maxMs ?? 30_000, + (options.baseMs ?? 250) * 2 ** attempt, + ); + const jitter = 0.5 + (options.random ?? Math.random)(); + await ( + options.sleep ?? + ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) + )(Math.min(options.maxMs ?? 30_000, base * jitter)); + } + } +} diff --git a/src/core/risk.ts b/src/core/risk.ts new file mode 100644 index 0000000..c81c73d --- /dev/null +++ b/src/core/risk.ts @@ -0,0 +1,101 @@ +import type { Config } from './config.js'; +import { AgentCliError } from '../errors/index.js'; +import { parseUsdc } from '../funding/amounts.js'; +export interface RiskIntent { + coin: string; + size: string; + price: string; + side: 'buy' | 'sell'; + leverage: number; + reduceOnly?: boolean; +} +export interface RiskState { + positionSize: string; + dailyLoss: string; + openOrders: number; + marketMaxLeverage: number; + pendingSize?: string; + pendingBuySize?: string; + pendingSellSize?: string; +} +const scale = 10n ** 18n; +const decimal = (value: string) => + value.startsWith('-') ? -parseUsdc(value.slice(1), 18) : parseUsdc(value, 18); +const absolute = (value: bigint) => (value < 0n ? -value : value); +export function evaluateRisk( + config: Config, + intent: RiskIntent, + state: RiskState, +) { + const fail = (limit: string, actual: unknown, maximum: unknown): never => { + throw new AgentCliError('POLICY_REJECTED', `Risk limit: ${limit}`, { + limit, + actual, + maximum, + }); + }; + if ( + config.allowedMarkets.length && + !config.allowedMarkets.includes(intent.coin) + ) + fail('allowedMarkets', intent.coin, config.allowedMarkets); + if ( + !Number.isInteger(intent.leverage) || + intent.leverage < 1 || + intent.leverage > + Math.min(config.limits.maxLeverage ?? Infinity, state.marketMaxLeverage) + ) + fail( + 'maxLeverage', + intent.leverage, + Math.min(config.limits.maxLeverage ?? Infinity, state.marketMaxLeverage), + ); + const size = decimal(intent.size), + price = decimal(intent.price); + if (size <= 0n || price <= 0n) + fail('positive size/price', intent.size, intent.price); + const notional = (size * price) / scale; + const signed = intent.side === 'buy' ? size : -size; + const position = decimal(state.positionSize); + if ( + intent.reduceOnly && + (position === 0n || + (position > 0n && signed > 0n) || + (position < 0n && signed < 0n)) + ) + fail('reduceOnly', intent.side, state.positionSize); + const effective = intent.reduceOnly + ? (position > 0n ? 1n : -1n) * + (absolute(position) - + (size > absolute(position) ? absolute(position) : size)) + : position + signed; + const legacy = decimal(state.pendingSize ?? '0'); + const buys = decimal( + state.pendingBuySize ?? (legacy > 0n ? state.pendingSize! : '0'), + ); + const sells = decimal( + state.pendingSellSize ?? (legacy < 0n ? state.pendingSize!.slice(1) : '0'), + ); + const long = absolute(effective + buys), + short = absolute(effective - sells); + const projected = long > short ? long : short; + const comparisons: [string, bigint, number | undefined][] = [ + ['maxOrderSize', size, config.limits.maxOrderSize], + ['maxNotional', notional, config.limits.maxNotional], + [ + 'maxPositionNotional', + (projected * price) / scale, + config.limits.maxPositionNotional, + ], + ['dailyLoss', decimal(state.dailyLoss), config.limits.dailyLoss], + ]; + for (const [name, actual, maximum] of comparisons) + if (maximum !== undefined && actual > decimal(String(maximum))) + fail(name, actual.toString(), maximum); + if ( + config.limits.maxOpenOrders !== undefined && + state.openOrders >= config.limits.maxOpenOrders + ) + fail('maxOpenOrders', state.openOrders, config.limits.maxOpenOrders); + return { allowed: true, notional: notional.toString() }; +} diff --git a/src/core/router.ts b/src/core/router.ts new file mode 100644 index 0000000..38cf556 --- /dev/null +++ b/src/core/router.ts @@ -0,0 +1,207 @@ +import { outputSchemas } from './output-schemas.js'; +import { errorDefaults } from '../errors/index.js'; +import { assertSafeInput } from '../wallet/safety.js'; +import { auditedAction } from './audit.js'; +import { z } from 'zod'; +import { AgentCliError, normalizeError, success } from '../errors/index.js'; +export interface Context { + configPath?: string; + network?: 'mainnet' | 'testnet'; + pretty: boolean; + runner?: boolean; + streaming?: boolean; +} +export interface Command { + description: string; + schema: z.ZodType; + execute: ( + input: Record, + context: Context, + ) => Promise; +} +export let routing: + | (( + name: string, + input: Record, + context: Context, + ) => Promise<{ routed: boolean; data?: unknown }>) + | undefined; +export function setRouting(value: typeof routing) { + routing = value; +} +export const commands = new Map(); +export function register(name: string, command: Command) { + if (command.schema instanceof z.ZodObject) + command = { ...command, schema: command.schema.strict() }; + if ( + ['order', 'cancel', 'modify', 'leverage', 'rotate', 'revoke'].includes(name) + ) { + const original = command.execute; + command = { + ...command, + schema: + command.schema instanceof z.ZodObject + ? command.schema.extend({ + intentId: z.string().optional(), + intent: z.string().optional(), + }) + : command.schema, + execute: (input, context) => + auditedAction(name, input, () => original(input, context)), + }; + } + commands.set(name, command); +} +export function schemas(name: string) { + const command = commands.get(name); + if (!command) + throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); + return { + input: z.toJSONSchema(command.schema), + output: { + oneOf: [ + { + type: 'object', + required: ['ok', 'data'], + properties: { ok: { const: true }, data: outputSchemas[name] ?? {} }, + }, + { + type: 'object', + required: ['ok', 'code', 'message', 'retryable', 'details'], + properties: { + ok: { const: false }, + code: { enum: Object.keys(errorDefaults) }, + message: { type: 'string' }, + retryable: { type: 'boolean' }, + details: {}, + }, + }, + ], + }, + }; +} +function parseJsonObject(value: string) { + const parsed: unknown = JSON.parse(value); + assertSafeInput(parsed); + return z.record(z.string(), z.unknown()).parse(parsed); +} +function parseValue(value: string): unknown { + try { + return JSON.parse(value); + } catch { + return value; + } +} +export async function dispatch(argv: string[], stdin = '') { + const input = Object.create(null) as Record; + const positional: string[] = []; + const context: Context = { pretty: false }; + try { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--pretty') { + context.pretty = true; + continue; + } + if (arg === '--help' || arg === '-h') { + positional.splice(0, positional.length, 'help'); + continue; + } + if (arg === '--config') { + context.configPath = argv[++i]; + if (!context.configPath) + throw new AgentCliError('CONFIG_INVALID', '--config requires a path'); + continue; + } + if (arg === '--network') { + context.network = z.enum(['mainnet', 'testnet']).parse(argv[++i]); + continue; + } + if (arg === '--json') { + Object.assign(input, parseJsonObject(argv[++i])); + continue; + } + if (arg.startsWith('--no-')) { + input[ + arg.slice(5).replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + ] = false; + continue; + } + if (arg.startsWith('--')) { + const [flag, inline] = arg.slice(2).split('=', 2); + const key = flag.replace(/-([a-z])/g, (_, c: string) => + c.toUpperCase(), + ); + const raw = + inline !== undefined + ? inline + : argv[i + 1] && !argv[i + 1].startsWith('--') + ? argv[++i] + : undefined; + input[key] = + raw === undefined + ? true + : [ + 'size', + 'price', + 'amount', + 'triggerPrice', + 'signature', + 'builderSignature', + 'cloid', + ].includes(key) + ? raw + : parseValue(raw); + continue; + } + positional.push(arg); + } + if (stdin.trim()) Object.assign(input, parseJsonObject(stdin)); + const [name = 'help', ...args] = positional; + if (name === 'help') + return { + body: success({ + commands: [...commands].map(([name, c]) => ({ + name, + description: c.description, + })), + globalFlags: ['--json', '--config', '--network', '--pretty'], + }), + exitCode: 0, + context, + }; + if (name === 'schema') + return { body: success(schemas(args[0])), exitCode: 0, context }; + const command = commands.get(name); + if (!command) + throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); + if (args.length) input.args = args; + assertSafeInput(input); + const parsed = command.schema.safeParse(input); + if (!parsed.success) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Invalid command input', + parsed.error.flatten(), + ); + const route = await routing?.( + name, + parsed.data as Record, + context, + ); + const data = route?.routed + ? route.data + : await command.execute(parsed.data as Record, context); + return { body: success(data), exitCode: 0, context }; + } catch (error) { + const normalized = + error instanceof z.ZodError || error instanceof SyntaxError + ? new AgentCliError('CONFIG_INVALID', 'Invalid JSON or flags') + : normalizeError(error); + return { + body: normalized.toJSON(), + exitCode: normalized.exitCode, + context, + }; + } +} diff --git a/src/core/skill-content.ts b/src/core/skill-content.ts new file mode 100644 index 0000000..8fd2c33 --- /dev/null +++ b/src/core/skill-content.ts @@ -0,0 +1,3 @@ +// Generated from SKILL.md and package.json. +export const cliVersion="0.1.0"; +export const skillContent="---\nname: agent-cli\ndescription: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery.\nmetadata:\n version: \"0.1.0\"\n---\n\nUse `agent-cli` for the user's authorized Hyperliquid account operations. Obtain\ncommand schemas with `agent-cli schema COMMAND`; arguments can be flags or\n`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one\nJSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`.\nLogs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines.\nUse `--network testnet` during testing; never silently change to mainnet.\n\n## Onboarding sequence\n\n1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP`\n and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets\n the environment secret outside the conversation. The default Bloxwap builder is\n `0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (1 bp = 0.01%).\n Override with `config set builder.address` / `config set builder.fee` or\n `BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`.\n2. Run `init --master-address 0x... --network testnet`. Show the returned approval\n payload/deeplink/QR. The user signs in their master wallet; submit only the\n resulting signature with `init --signature 0x...`. Submit builder approval\n separately with `init --builder-signature 0x...` for the configured 0.01% default. Continue only after `approved:true`.\n3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address,\n origin chain/token, minimum, fees, expected received amount and expiry. On\n testnet, do not treat the default Arbitrum mainnet chain/token as a test route:\n use an explicitly supported test chain/token and `originDecimals`, or the\n credential-gated CI harness with mocked Relay and pre-funded test collateral.\n If no test route is known, stop funding and report the missing route. The user\n transfers the quoted origin token. If `signatureSteps` require signatures,\n show the exact payload and submit each via `deposit` in returned order.\n4. Poll `status` until each active deposit is `settled`, for at most 12 polls\n or 5 minutes, whichever comes first. Start with 1-second delays, double to a\n 15-second cap, and stop with an explicit pending report if the budget expires.\n Settlement requires Relay success and Hyperliquid balance receipt. Stop on\n failed/refunded deposits and explain their status. Do not place orders while\n any deposit is awaiting funds or pending.\n5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the\n last handled sequence; replay after reconnect. An unfilled or rejected order\n remains incomplete: do not claim the requested trade succeeded. A fill updates the trading\n plan, an error follows the catalog below, and expiry warnings prompt rotation\n using external master-wallet approval.\n\n## Commands and example envelopes\n\nExamples show representative `data`, not live balances or service promises.\nFull inputs/outputs are discoverable with `schema`; amounts/prices are strings.\n\n| Input | Example output |\n| --- | --- |\n| `--help` | `{\"ok\":true,\"data\":{\"commands\":[{\"name\":\"status\",\"description\":\"Read account status\"}],\"globalFlags\":[\"--json\",\"--config\",\"--network\",\"--pretty\"]}}` |\n| `schema order` | `{\"ok\":true,\"data\":{\"input\":{\"type\":\"object\"},\"output\":{\"oneOf\":[]}}}` (actual schemas include properties) |\n| `config get network` | `{\"ok\":true,\"data\":\"testnet\"}` |\n| `config set limits.maxLeverage 5` | `{\"ok\":true,\"data\":{\"version\":1,\"limits\":{\"maxLeverage\":5}}}` |\n| `config validate` | `{\"ok\":true,\"data\":{\"valid\":true,\"version\":1}}` |\n| `init --master-address 0x...` | `{\"ok\":true,\"data\":{\"approved\":false,\"requiresSignature\":true,\"approvals\":{\"agent\":{\"typedData\":{}}},\"qrAscii\":\"...\"}}` |\n| `fund --amount 10 --no-qr` | `{\"ok\":true,\"data\":{\"depositId\":\"...\",\"depositAddress\":\"0x...\",\"expectedReceived\":\"100000000\",\"builderFeeRate\":\"0.01%\",\"builderAddress\":\"0x71b09A08257078A4d642F7Dd7315e656c837329a\",\"state\":\"awaiting_funds\",\"qrAscii\":null}}` |\n| `deposit --deposit-id ID` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[{\"id\":\"step:0\",\"requiresSignature\":true,\"payload\":{}}]}}` |\n| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[]}}` |\n| `status` | `{\"ok\":true,\"data\":{\"balance\":{\"accountValue\":\"100\"},\"agent\":{\"approved\":true,\"daysRemaining\":\"179\"},\"deposits\":[],\"policy\":{\"id\":\"...\",\"version\":\"1\"},\"runner\":{\"pid\":\"123\"}}}` |\n| `balance` | `{\"ok\":true,\"data\":{\"accountValue\":\"100\",\"accountValueUnits\":\"10000000000\",\"withdrawable\":\"96\",\"marginUsed\":\"4\"}}` |\n| `market --coin BTC` | `{\"ok\":true,\"data\":{\"coin\":\"BTC\",\"asset\":\"0\",\"szDecimals\":\"5\",\"maxLeverage\":\"40\",\"mid\":\"20000\"}}` |\n| `positions` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"size\":\"0.001\",\"entryPrice\":\"20000\",\"leverage\":\"5\",\"marginMode\":\"cross\"}]}` |\n| `orders` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"oid\":\"123\",\"sz\":\"0.001\",\"limitPx\":\"20000\",\"side\":\"B\"}]}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"buy\",\"size\":\"0.001\",\"price\":\"20000\",\"tif\":\"Gtc\",\"cloid\":\"0x11111111111111111111111111111111\"}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"resting\":{\"oid\":123}}]}}},\"builderFeeRate\":\"0.01%\",\"builderAddress\":\"0x71b09A08257078A4d642F7Dd7315e656c837329a\",\"cloid\":\"0x...\"}}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"sell\",\"size\":\"0.001\",\"type\":\"market\",\"slippage\":0.01,\"reduceOnly\":true}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"filled\":{\"oid\":123}}]}}}}}` |\n| `cancel --oid 123` or `cancel --all` | `{\"ok\":true,\"data\":{\"cancelled\":[\"123\"],\"result\":{\"status\":\"ok\"}}}` |\n| `modify --json '{\"modifications\":[{\"oid\":123,\"price\":\"20100\",\"size\":\"0.001\"}]}'` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `leverage --coin BTC --leverage 5 --mode isolated` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `run --detach` | `{\"ok\":true,\"data\":{\"pid\":123,\"startedAt\":1700000000000,\"network\":\"testnet\"}}` |\n| `events --since 12 --types '[\"fill\"]'` | `{\"ok\":true,\"data\":[{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}]}` |\n| `events --follow` | `{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}` (one line per event) |\n| `logs --follow --level warn` | `{\"level\":\"warn\",\"requestId\":\"...\",\"message\":\"Risk rejected\",\"time\":\"...\"}` (JSON lines) |\n| `rotate` then `rotate --signature 0x...` | `{\"ok\":true,\"data\":{\"oldAgent\":\"0x...\",\"newAgent\":\"0x...\",\"swapped\":true,\"oldRevoked\":false,\"revokeApproval\":{}}}` |\n| `rotate --revoke-signature 0x...` | `{\"ok\":true,\"data\":{\"swapped\":true,\"oldRevoked\":true}}` |\n| `revoke --cancel-orders` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":false,\"requiresSignature\":true,\"approval\":{}}}` |\n| `revoke --signature 0x...` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":true}}` |\n| `audit --intent ID` | `{\"ok\":true,\"data\":[{\"intentId\":\"ID\",\"stage\":\"decision\",\"data\":{\"allowed\":true},\"previousHash\":\"...\",\"hash\":\"...\"}]}` |\n| `skill install --target codex` | `{\"ok\":true,\"data\":{\"target\":\"codex\",\"path\":\".../agent-cli/SKILL.md\",\"version\":\"0.1.0\"}}` |\n\nBefore an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and\nmarket leverage ceiling. Compute size/notional with decimal strings/bigints,\nusing the user-approved limit or the bounded IOC execution price. A valid schema\nalone does not establish that an order is tradable or within the budget.\n\nUse `--intent-id ID --intent \"reason\"` on trading actions to connect the user's\nreasoning to the audit. Keep cloid stable when retrying a known order intent.\nFor TP/SL use `type:\"tp\"|\"sl\"` and `triggerPrice` with size/price/reduceOnly;\nmarket orders use an explicit slippage-bounded IOC, not an unbounded market order.\n\n## Error recovery\n\n| Code | Action |\n| --- | --- |\n| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results |\n| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent |\n| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure |\n| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback |\n| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding |\n| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy |\n| AGENT_EXPIRED | Rotate with master-wallet approval |\n| AGENT_NOT_APPROVED | Complete init/approval or explain revocation |\n| INVALID_SIZE, INVALID_PRICE | Correct input using market precision |\n| UNKNOWN_MARKET | Refresh metadata; use an allowed market |\n| ORDER_NOT_FOUND | Refresh orders before changing intent |\n| CONFIG_INVALID | Correct configuration/input; never insert secrets |\n| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure |\n\n## Gotchas\n\nHyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and\nbigints. Some deposits require **two distinct signatures**; resume stored steps\nrather than restarting the route. Agent expiry is at most **180 days**. Never\nask for, paste, log or import a master private key or seed. The managed agent is\ntrade-only; local limits include markets, size/notional, leverage, daily loss and\nopen orders. Privy sees an L1 action hash, so it cannot independently inspect\nnotional/action contents; Hyperliquid agent permissions and local checks enforce\nthose boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`.\n"; diff --git a/src/core/trading.ts b/src/core/trading.ts new file mode 100644 index 0000000..1bd0e3c --- /dev/null +++ b/src/core/trading.ts @@ -0,0 +1,156 @@ +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +import { decisionAudit } from './audit.js'; +import { assertAgentValid } from '../wallet/expiry.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { type Config } from './config.js'; +import { + createClients, + type ExchangeAdapter, + type Market, +} from './hlclient.js'; +import { privyClient, privySigner } from '../wallet/privy.js'; +import { nextNonce } from './nonce.js'; +import { hlInfo } from './http.js'; +import { type AccountState, type OpenOrder } from '../commands/read.js'; +import { AgentCliError } from '../errors/index.js'; +import { assertDepositsSettled } from '../funding/status.js'; +import { assertTradeAction } from '../wallet/policy.js'; +import { mapHyperliquidError } from '../errors/mappers.js'; +import { evaluateRisk, type RiskIntent } from './risk.js'; +import { Logger } from './logger.js'; +export interface TradingServices { + exchange: ExchangeAdapter; + market: (coin: string) => Promise; + state: () => Promise; + orders: () => Promise; + mids: () => Promise>; + dailyLoss?: () => Promise; +} +export async function tradingServices( + config: Config, +): Promise { + const signer = + activeRunner?.signer ?? + (await privySigner(config, await privyClient(config))); + const clients = createClients(config.network, signer, () => + nextNonce(signer.address), + ); + return { + exchange: clients.exchange!, + market: (coin) => clients.meta.lookup(coin), + state: () => + hlInfo(config.network, 'clearinghouseState', { + user: config.masterAddress, + }), + orders: () => + hlInfo(config.network, 'frontendOpenOrders', { + user: config.masterAddress, + }), + mids: () => hlInfo(config.network, 'allMids'), + dailyLoss: async () => + dailyLossFromPortfolio( + await hlInfo(config.network, 'portfolio', { + user: config.masterAddress, + }), + ), + }; +} +export async function assertCanTrade(config: Config) { + if (config.revoked || !config.approved || !config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Agent approval required'); + assertAgentValid(config); + await assertDepositsSettled(config.network, config.masterAddress); +} +export async function checkRisk( + config: Config, + intent: RiskIntent, + services: TradingServices, + ignoreOid?: number, +) { + const [market, state, orders] = await Promise.all([ + services.market(intent.coin), + services.state(), + services.orders(), + ]); + const position = state.assetPositions.find( + (p) => p.position.coin === intent.coin, + )?.position; + const open = orders.filter((o) => o.oid !== ignoreOid); + const pending = open.filter((o) => o.coin === intent.coin && !o.reduceOnly); + const buys = pending + .filter((o) => o.side === 'B') + .reduce((sum, o) => sum + parseUsdc(o.sz, 18), 0n), + sells = pending + .filter((o) => o.side === 'A') + .reduce((sum, o) => sum + parseUsdc(o.sz, 18), 0n); + const loss = + config.limits.dailyLoss !== undefined + ? await requiredDailyLoss(services) + : '0'; + const logger = new Logger({ level: config.logLevel }); + try { + return evaluateRisk(config, intent, { + positionSize: position?.szi ?? '0', + dailyLoss: loss, + openOrders: open.length, + marketMaxLeverage: market.maxLeverage, + pendingBuySize: formatUsdc(buys, 18), + pendingSellSize: formatUsdc(sells, 18), + }); + } catch (error) { + logger.log('warn', 'Risk rejected', { + error: mapHyperliquidError(error).toJSON(), + }); + await logger.flush(); + throw error; + } +} +export async function exchangeAction( + action: string, + operation: () => Promise, +) { + assertTradeAction(action); + await decisionAudit({ allowed: true, action }); + try { + const response = await operation(); + const data = response as { + status?: string; + response?: { data?: { statuses?: unknown[] } }; + }; + if (data.status === 'err') throw mapHyperliquidError(response); + for (const status of data.response?.data?.statuses ?? []) + if (status && typeof status === 'object' && 'error' in status) + throw mapHyperliquidError((status as { error: string }).error); + return response; + } catch (error) { + throw mapHyperliquidError(error); + } +} + +export function dailyLossFromPortfolio( + portfolio: [string, { pnlHistory: [number, string][] }][], + now = Date.now(), +) { + const history = portfolio.find(([period]) => period === 'day')?.[1] + .pnlHistory; + if (!history?.length) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Daily loss history unavailable', + ); + const midnight = new Date(now); + midnight.setUTCHours(0, 0, 0, 0); + const initial = + [...history].reverse().find(([time]) => time <= midnight.getTime()) ?? + history[0]; + const last = history.at(-1)!; + const signed = (value: string) => + value.startsWith('-') ? -parseUsdc(value.slice(1), 8) : parseUsdc(value, 8); + const pnl = signed(last[1]) - signed(initial[1]); + return formatUsdc(pnl < 0n ? -pnl : 0n, 8); +} +export async function requiredDailyLoss(services: TradingServices) { + if (!services.dailyLoss) + throw new AgentCliError('POLICY_REJECTED', 'Daily loss check unavailable'); + return services.dailyLoss(); +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..bcbd186 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,54 @@ +import { redact } from '../core/logger.js'; +export const errorDefaults = { + INSUFFICIENT_MARGIN: false, + POLICY_REJECTED: false, + AGENT_EXPIRED: false, + AGENT_NOT_APPROVED: false, + DEPOSIT_PENDING: true, + NONCE_COLLISION: true, + RATE_LIMIT: true, + INVALID_SIZE: false, + INVALID_PRICE: false, + UNKNOWN_MARKET: false, + ORDER_NOT_FOUND: false, + RUNNER_NOT_RUNNING: true, + CONFIG_INVALID: false, + NETWORK_ERROR: true, + UPSTREAM_ERROR: false, + INTERNAL: false, +} as const; +export type ErrorCode = keyof typeof errorDefaults; +export class AgentCliError extends Error { + readonly retryable: boolean; + constructor( + readonly code: ErrorCode, + message: string, + readonly details: unknown = null, + retryable = errorDefaults[code] as boolean, + ) { + super(message); + this.name = 'AgentCliError'; + this.retryable = retryable; + } + toJSON() { + return { + ok: false as const, + code: this.code, + message: String(redact(this.message)), + retryable: this.retryable, + details: redact(this.details), + }; + } + get exitCode() { + return this.code === 'CONFIG_INVALID' ? 2 : this.retryable ? 75 : 1; + } +} +export function normalizeError(error: unknown): AgentCliError { + return error instanceof AgentCliError + ? error + : new AgentCliError( + 'INTERNAL', + error instanceof Error ? error.message : String(error), + ); +} +export const success = (data: T) => ({ ok: true as const, data }); diff --git a/src/errors/mappers.ts b/src/errors/mappers.ts new file mode 100644 index 0000000..0d62640 --- /dev/null +++ b/src/errors/mappers.ts @@ -0,0 +1,54 @@ +import { AgentCliError, type ErrorCode } from './index.js'; +import { redact } from '../core/logger.js'; +function describe(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return JSON.stringify(error) ?? 'Unknown upstream error'; +} +function map(error: unknown, service: string): AgentCliError { + if (error instanceof AgentCliError) return error; + const message = String(redact(describe(error))); + if ( + (error instanceof TypeError && /fetch|network/i.test(message)) || + /ECONNRESET|ETIMEDOUT|ENOTFOUND|ECONNREFUSED/.test(message) + ) + return mapNetworkError(error); + let code: ErrorCode = 'UPSTREAM_ERROR'; + const patterns: [RegExp, ErrorCode][] = [ + [/429|rate.?limit|too many requests/i, 'RATE_LIMIT'], + [/nonce|duplicate.*timestamp/i, 'NONCE_COLLISION'], + [/insufficient.*margin|margin.*insufficient/i, 'INSUFFICIENT_MARGIN'], + [ + /minimum.*(size|value)|min.*notional|invalid.*size|too small/i, + 'INVALID_SIZE', + ], + [/tick|invalid.*price/i, 'INVALID_PRICE'], + [/policy|not allowed|denied/i, 'POLICY_REJECTED'], + [/agent.*expired/i, 'AGENT_EXPIRED'], + [/agent.*not.*approved|not.*authorized/i, 'AGENT_NOT_APPROVED'], + [/order.*not.*found|does not exist/i, 'ORDER_NOT_FOUND'], + ]; + for (const [pattern, candidate] of patterns) + if (pattern.test(message)) { + code = candidate; + break; + } + return new AgentCliError( + code, + message, + redact({ + service, + upstream: error instanceof Error ? { message: error.message } : error, + }), + ); +} +export const mapHyperliquidError = (error: unknown) => + map(error, 'hyperliquid'); +export const mapPrivyError = (error: unknown) => map(error, 'privy'); +export const mapRelayError = (error: unknown) => map(error, 'relay'); +export const mapNetworkError = (error: unknown) => + new AgentCliError( + 'NETWORK_ERROR', + String(redact(describe(error))), + redact({ service: 'network', upstream: describe(error) }), + ); diff --git a/src/funding/amounts.ts b/src/funding/amounts.ts new file mode 100644 index 0000000..0e49aaa --- /dev/null +++ b/src/funding/amounts.ts @@ -0,0 +1,42 @@ +import { AgentCliError } from '../errors/index.js'; +function checkDecimals(decimals: number) { + if (!Number.isInteger(decimals) || decimals < 0 || decimals > 30) + throw new RangeError('Invalid decimals'); +} +export function parseUsdc(value: string, decimals: number): bigint { + checkDecimals(decimals); + if (!/^-?\d+(\.\d+)?$/.test(value)) + throw new AgentCliError( + 'INVALID_SIZE', + 'Expected nonnegative decimal string', + ); + const negative = value.startsWith('-'); + const [whole, fraction = ''] = (negative ? value.slice(1) : value).split('.'); + if (fraction.length > decimals) + throw new AgentCliError('INVALID_SIZE', 'Too many decimal places'); + return ( + (negative ? -1n : 1n) * + (BigInt(whole) * 10n ** BigInt(decimals) + + BigInt(fraction.padEnd(decimals, '0') || '0')) + ); +} +export function formatUsdc(value: bigint, decimals: number): string { + checkDecimals(decimals); + const negative = value < 0n; + const absolute = negative ? -value : value; + const scale = 10n ** BigInt(decimals); + const fraction = (absolute % scale) + .toString() + .padStart(decimals, '0') + .replace(/0+$/, ''); + return `${negative ? '-' : ''}${absolute / scale}${fraction ? '.' + fraction : ''}`; +} +export const toHyperCore = (evm: bigint) => evm * 100n; +export function fromHyperCore(core: bigint) { + if (core % 100n !== 0n) + throw new AgentCliError( + 'INVALID_SIZE', + 'Conversion would lose HyperCore precision', + ); + return core / 100n; +} diff --git a/src/funding/qr.ts b/src/funding/qr.ts new file mode 100644 index 0000000..f33afdb --- /dev/null +++ b/src/funding/qr.ts @@ -0,0 +1,32 @@ +import QRCode from 'qrcode'; +import { writeFile } from 'node:fs/promises'; +export async function renderQr( + value: string, + options: { + enabled?: boolean; + path?: string; + stderr?: (value: string) => void; + } = {}, +) { + if (options.enabled === false) return { qrAscii: null, qrPath: null }; + const qrAscii = await QRCode.toString(value, { + type: 'terminal', + small: true, + }); + (options.stderr ?? ((value) => process.stderr.write(value)))(qrAscii + '\n'); + if (options.path) { + if (options.path.endsWith('.svg')) + await writeFile( + options.path, + await QRCode.toString(value, { type: 'svg' }), + { mode: 0o600 }, + ); + else + await writeFile( + options.path, + await QRCode.toBuffer(value, { type: 'png' }), + { mode: 0o600 }, + ); + } + return { qrAscii, qrPath: options.path ?? null }; +} diff --git a/src/funding/relay.ts b/src/funding/relay.ts new file mode 100644 index 0000000..c880c18 --- /dev/null +++ b/src/funding/relay.ts @@ -0,0 +1,98 @@ +import { z } from 'zod'; +import { jsonRequest } from '../core/http.js'; +import { mapRelayError } from '../errors/mappers.js'; +export const PERPS_USDC = '0x00000000000000000000000000000000'; +export interface QuoteRequest { + user: string; + recipient: string; + originChainId: number; + originCurrency: string; + amount: string; + destinationChainId: 1337; + destinationCurrency: typeof PERPS_USDC; + useDepositAddress: true; + tradeType: 'EXACT_INPUT'; +} +const item = z + .object({ + status: z.string(), + data: z.record(z.string(), z.unknown()), + check: z.object({ endpoint: z.string(), method: z.string() }).optional(), + }) + .passthrough(); +export const quoteSchema = z + .object({ + steps: z.array( + z + .object({ + id: z.string(), + kind: z.string(), + requestId: z.string().optional(), + depositAddress: z.string().optional(), + items: z.array(item), + }) + .passthrough(), + ), + fees: z.record(z.string(), z.unknown()).default({}), + details: z + .object({ + currencyOut: z + .object({ + amount: z.string(), + currency: z.object({ decimals: z.number() }).passthrough(), + }) + .passthrough(), + currencyIn: z.object({ amount: z.string() }).passthrough().optional(), + minAmount: z.string().optional(), + expiresAt: z.number().optional(), + }) + .passthrough(), + }) + .passthrough(); +export type RelayQuote = z.infer; +export class RelayClient { + constructor( + readonly base = 'https://api.relay.link', + private fetcher: typeof fetch = fetch, + ) {} + async quote(request: QuoteRequest) { + try { + return quoteSchema.parse( + await jsonRequest(`${this.base}/quote/v2`, request, this.fetcher), + ); + } catch (error) { + throw mapRelayError(error); + } + } + async status(requestId: string) { + try { + return (await jsonRequest( + `${this.base}/intents/status/v3?requestId=${encodeURIComponent(requestId)}`, + undefined, + this.fetcher, + )) as { status: string }; + } catch (error) { + throw mapRelayError(error); + } + } +} +export function parseQuote(quote: RelayQuote) { + const deposit = quote.steps.find((step) => step.depositAddress); + if (!deposit?.requestId) + throw mapRelayError('Quote missing deposit address/requestId'); + return { + depositId: deposit.requestId, + depositAddress: deposit.depositAddress!, + fees: { + app: quote.fees.app ?? quote.fees.appFees ?? null, + gas: quote.fees.gas ?? null, + relayer: quote.fees.relayer ?? quote.fees.relayerService ?? null, + raw: quote.fees, + }, + minAmount: quote.details.minAmount ?? '0', + expectedReceived: quote.details.currencyOut.amount, + receivedDecimals: quote.details.currencyOut.currency.decimals, + expiresAt: quote.details.expiresAt ?? Date.now() + 30_000, + steps: quote.steps, + }; +} diff --git a/src/funding/signatures.ts b/src/funding/signatures.ts new file mode 100644 index 0000000..f21858e --- /dev/null +++ b/src/funding/signatures.ts @@ -0,0 +1,88 @@ +import { AgentCliError } from '../errors/index.js'; +import { jsonRequest } from '../core/http.js'; +import { readDeposits, writeDeposits, type Deposit } from './store.js'; +export function signatureSteps(deposit: Deposit) { + return deposit.quote.steps.flatMap((step) => + step.kind === 'signature' + ? step.items.map((item, index) => ({ + id: `${step.id}:${index}`, + requiresSignature: !deposit.completedSteps.includes( + `${step.id}:${index}`, + ), + payload: signaturePayload(item.data.sign), + deeplink: `ethereum:${deposit.recipient}?request=${encodeURIComponent(JSON.stringify(signaturePayload(item.data.sign)))}`, + post: item.data.post, + })) + : [], + ); +} +export async function submitDepositSignature( + depositId: string, + stepId: string, + signature: string, + options: { + path?: string; + base?: string; + submit?: (url: string, body: unknown) => Promise; + } = {}, +) { + if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) + throw new AgentCliError('CONFIG_INVALID', 'Expected wallet signature'); + const deposits = await readDeposits(options.path); + const deposit = deposits.find((d) => d.id === depositId); + if (!deposit) throw new AgentCliError('CONFIG_INVALID', 'Unknown deposit'); + const pending = signatureSteps(deposit).filter((s) => s.requiresSignature); + if (!pending.length) return { complete: true }; + const step = pending[0]; + if (step.id !== stepId) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Submit signatures in step order', + { nextStep: step.id }, + ); + const post = step.post as { + endpoint: string; + method: string; + body: Record; + }; + if ( + !post || + post.method !== 'POST' || + !post.endpoint.startsWith('/') || + post.endpoint.startsWith('//') + ) + throw new AgentCliError( + 'UPSTREAM_ERROR', + 'Invalid signature submission endpoint', + ); + const base = + options.base ?? + (deposit.network === 'testnet' + ? 'https://api.testnets.relay.link' + : 'https://api.relay.link'); + const url = new URL(post.endpoint, base); + if (url.origin !== new URL(base).origin) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Untrusted signature destination', + ); + // Relay's generic step contract puts the wallet signature in the query. + url.searchParams.set('signature', signature); + await (options.submit ?? jsonRequest)(url.toString(), post.body); + deposit.completedSteps.push(step.id); + await writeDeposits(deposits, options.path); + return { depositId, steps: signatureSteps(deposit) }; +} + +function signaturePayload(value: unknown) { + if (!value || typeof value !== 'object') return value; + const sign = value as Record; + if (sign.signatureKind === 'eip712') + return { + domain: sign.domain, + types: sign.types, + primaryType: sign.primaryType, + message: sign.message ?? sign.value, + }; + return value; +} diff --git a/src/funding/status.ts b/src/funding/status.ts new file mode 100644 index 0000000..46c4a8c --- /dev/null +++ b/src/funding/status.ts @@ -0,0 +1,88 @@ +import { readDeposits, writeDeposits, type Deposit } from './store.js'; +import { RelayClient, parseQuote } from './relay.js'; +import { parseUsdc } from './amounts.js'; +import { AgentCliError } from '../errors/index.js'; +export function advanceDeposit( + deposit: Deposit, + relayStatus: string, + balance: string, +) { + if (['settled', 'failed', 'refunded'].includes(deposit.state)) + return deposit.state; + if (relayStatus === 'failure' || relayStatus === 'failed') + return 'failed' as const; + if (relayStatus === 'refunded') return 'refunded' as const; + const quote = parseQuote(deposit.quote); + const needed = + quote.receivedDecimals <= 8 + ? BigInt(quote.expectedReceived) * + 10n ** BigInt(8 - quote.receivedDecimals) + : BigInt(quote.expectedReceived) / + 10n ** BigInt(quote.receivedDecimals - 8); + if ( + relayStatus === 'success' && + parseUsdc(balance, 8) >= parseUsdc(deposit.baseline, 8) + needed + ) + return 'settled' as const; + if ( + ['success', 'pending', 'deposit-confirmed', 'waiting'].includes(relayStatus) + ) + return 'pending' as const; + return deposit.state; +} +export async function pollDeposits( + network: Deposit['network'], + recipient: string, + balance: string, + options: { + path?: string; + relay?: RelayClient; + emit?: (deposit: Deposit) => Promise; + } = {}, +) { + const deposits = await readDeposits(options.path); + let changed = false; + for (const deposit of deposits.filter( + (d) => + d.network === network && + d.recipient.toLowerCase() === recipient.toLowerCase(), + )) { + if (['settled', 'failed', 'refunded'].includes(deposit.state)) continue; + const status = await ( + options.relay ?? + new RelayClient( + network === 'testnet' + ? 'https://api.testnets.relay.link' + : 'https://api.relay.link', + ) + ).status(deposit.id); + const next = advanceDeposit(deposit, status.status, balance); + if (next !== deposit.state) { + deposit.state = next; + changed = true; + await options.emit?.(deposit); + } + } + if (changed) await writeDeposits(deposits, options.path); + return deposits.filter( + (d) => + d.network === network && + d.recipient.toLowerCase() === recipient.toLowerCase(), + ); +} +export async function assertDepositsSettled( + network: Deposit['network'], + recipient: string, + path?: string, +) { + const pending = (await readDeposits(path)).filter( + (d) => + d.network === network && + d.recipient.toLowerCase() === recipient.toLowerCase() && + ['awaiting_funds', 'pending'].includes(d.state), + ); + if (pending.length) + throw new AgentCliError('DEPOSIT_PENDING', 'Wait for deposit settlement', { + depositIds: pending.map((d) => d.id), + }); +} diff --git a/src/funding/store.ts b/src/funding/store.ts new file mode 100644 index 0000000..b456e30 --- /dev/null +++ b/src/funding/store.ts @@ -0,0 +1,28 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite, stateDir } from '../core/config.js'; +import type { RelayQuote } from './relay.js'; +export interface Deposit { + id: string; + network: 'mainnet' | 'testnet'; + recipient: string; + route: string; + quote: RelayQuote; + expiresAt: number; + state: 'awaiting_funds' | 'pending' | 'settled' | 'failed' | 'refunded'; + baseline: string; + createdAt: number; + completedSteps: string[]; +} +export const depositPath = () => join(stateDir(), 'deposits.json'); +export async function readDeposits(path = depositPath()): Promise { + try { + return JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } +} +export async function writeDeposits(deposits: Deposit[], path = depositPath()) { + await atomicWrite(path, JSON.stringify(deposits)); +} diff --git a/src/runner/events.ts b/src/runner/events.ts new file mode 100644 index 0000000..5354d41 --- /dev/null +++ b/src/runner/events.ts @@ -0,0 +1,138 @@ +import { parseUsdc } from '../funding/amounts.js'; +import { AgentCliError } from '../errors/index.js'; +import { auditFill } from '../core/audit.js'; +import { + appendFile, + mkdir, + readFile, + chmod, + open, + unlink, +} from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { z } from 'zod'; +import { stateDir } from '../core/config.js'; +import { redact } from '../core/logger.js'; +export const eventTypes = [ + 'fill', + 'order_update', + 'position_change', + 'deposit', + 'error', + 'agent_expiry_warning', + 'runner_state', +] as const; +export type EventType = (typeof eventTypes)[number]; +export const eventSchema = z.object({ + seq: z.number().int().positive(), + timestamp: z.string(), + type: z.enum(eventTypes), + data: z.unknown(), +}); +export type RunnerEvent = z.infer; +export async function readEvents( + path = join(stateDir(), 'events.jsonl'), + since = 0, + types: EventType[] = [], +): Promise { + let contents: string; + try { + contents = await readFile(path, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } + return contents + .split('\n') + .slice(0, -1) + .filter(Boolean) + .map((line) => eventSchema.parse(JSON.parse(line))) + .filter( + (event) => + event.seq > since && (!types.length || types.includes(event.type)), + ); +} +export class EventStore { + private queue = Promise.resolve(); + constructor(readonly path = join(stateDir(), 'events.jsonl')) {} + append(type: EventType, data: unknown) { + let event: RunnerEvent; + const operation = this.queue.then(async () => { + if (type === 'fill' && data && typeof data === 'object') + await auditFill(data as Record); + await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }); + await chmod(dirname(this.path), 0o700); + let guard; + for (let attempt = 0; attempt < 100; attempt++) { + try { + guard = await open(this.path + '.lock', 'wx', 0o600); + break; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + if (!guard) + throw new AgentCliError('INTERNAL', 'Event journal lock busy'); + try { + const previous = (await readEvents(this.path)).at(-1)?.seq ?? 0; + event = eventSchema.parse({ + seq: previous + 1, + timestamp: new Date().toISOString(), + type, + data: redact(type === 'fill' ? normalizeFillAmounts(data) : data), + }); + await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }); + await appendFile(this.path, JSON.stringify(event) + '\n', { + mode: 0o600, + }); + await chmod(this.path, 0o600); + return event; + } finally { + await guard.close(); + await unlink(this.path + '.lock'); + } + }); + this.queue = operation.then(() => {}); + return operation; + } + async flush() { + await this.queue; + } +} +export async function followEvents(options: { + path?: string; + since?: number; + types?: EventType[]; + signal?: AbortSignal; + write: (event: RunnerEvent) => void; + interval?: number; +}) { + let since = options.since ?? 0; + while (!options.signal?.aborted) { + for (const event of await readEvents(options.path, since, options.types)) { + options.write(event); + since = event.seq; + } + await new Promise((resolve) => { + const timer = setTimeout(done, options.interval ?? 250); + function done() { + clearTimeout(timer); + options.signal?.removeEventListener('abort', done); + resolve(); + } + options.signal?.addEventListener('abort', done, { once: true }); + }); + } +} +export const events = new EventStore(); + +function normalizeFillAmounts(data: unknown) { + if (!data || typeof data !== 'object') return data; + const fill = data as Record; + if (typeof fill.fee !== 'string') return fill; + const units = fill.fee.startsWith('-') + ? -parseUsdc(fill.fee.slice(1), 8) + : parseUsdc(fill.fee, 8); + return { ...fill, feeUnits: units.toString() }; +} diff --git a/src/runner/health.ts b/src/runner/health.ts new file mode 100644 index 0000000..a39c2a9 --- /dev/null +++ b/src/runner/health.ts @@ -0,0 +1,72 @@ +import { collateralBalance } from '../core/account.js'; +import { expiryWarnings } from '../wallet/expiry.js'; +import { activeRunner, runningInfo, type Runner } from './lifecycle.js'; +import type { Subscriptions } from './subscriptions.js'; +import { events } from './events.js'; +import { hlInfo } from '../core/http.js'; +import { pollDeposits } from '../funding/status.js'; +import { normalizeError } from '../errors/index.js'; +export function health(runner: Runner, now = Date.now()) { + const subscriptions = runner.subscriptions; + return { + pid: String(runner.info.pid), + uptimeSeconds: String(Math.max(0, (now - runner.info.startedAt) / 1000)), + wsState: subscriptions?.state ?? 'disconnected', + wsStale: !subscriptions || now - subscriptions.lastMessage > 90_000, + lastFillTime: subscriptions?.lastFill ?? null, + signingSession: runner.signer ? 'ready' : 'unavailable', + }; +} +export async function runnerHealth() { + if (activeRunner) return health(activeRunner); + const info = await runningInfo(); + return info + ? { + pid: String(info.pid), + uptimeSeconds: String((Date.now() - info.startedAt) / 1000), + wsState: 'unknown', + signingSession: 'unknown', + } + : null; +} +export function startHeartbeat(runner: Runner, subscriptions: Subscriptions) { + runner.subscriptions = subscriptions; + let queue = Promise.resolve(); + const tick = () => { + queue = queue + .then(async () => { + await events.append('runner_state', health(runner)); + await expiryWarnings(runner.config); + if (runner.config.masterAddress) { + const state = await hlInfo<{ + marginSummary: { accountValue: string }; + assetPositions: { position: { unrealizedPnl: string } }[]; + }>(runner.config.network, 'clearinghouseState', { + user: runner.config.masterAddress, + }); + await pollDeposits( + runner.config.network, + runner.config.masterAddress, + collateralBalance(state), + { + emit: async (deposit) => { + await events.append('deposit', { + id: deposit.id, + state: deposit.state, + }); + }, + }, + ); + } + }) + .catch(async (error) => { + await events.append('error', normalizeError(error).toJSON()); + }); + }; + tick(); + const timer = setInterval(tick, 30_000); + runner.onShutdown(async () => { + clearInterval(timer); + await queue; + }); +} diff --git a/src/runner/lifecycle.ts b/src/runner/lifecycle.ts new file mode 100644 index 0000000..658a944 --- /dev/null +++ b/src/runner/lifecycle.ts @@ -0,0 +1,180 @@ +import type { Subscriptions } from './subscriptions.js'; +import { open, readFile, unlink, mkdir, chmod } from 'node:fs/promises'; +import { join } from 'node:path'; +import { spawn } from 'node:child_process'; +import { stateDir, atomicWrite, type Config } from '../core/config.js'; +import { Logger } from '../core/logger.js'; +import { privyClient, privySigner } from '../wallet/privy.js'; +import type { Signer } from '../core/hlclient.js'; +import { AgentCliError } from '../errors/index.js'; +export interface RunnerInfo { + pid: number; + startedAt: number; + network: Config['network']; + ready: boolean; +} +export const runnerPidPath = () => join(stateDir(), 'runner.pid'); +export function isAlive(pid: number) { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return (error as NodeJS.ErrnoException).code === 'EPERM'; + } +} +export async function runningInfo( + path = runnerPidPath(), +): Promise { + try { + const info = JSON.parse(await readFile(path, 'utf8')) as RunnerInfo; + return Number.isInteger(info.pid) && info.pid > 0 && isAlive(info.pid) + ? info + : null; + } catch (error) { + if ( + (error as NodeJS.ErrnoException).code === 'ENOENT' || + error instanceof SyntaxError + ) + return null; + throw error; + } +} +export class Runner { + subscriptions?: Subscriptions; + readonly info: RunnerInfo; + readonly logger: Logger; + signer?: Signer; + private timer?: NodeJS.Timeout; + private cleanup: (() => Promise)[] = []; + private closing = false; + constructor( + readonly config: Config, + readonly directory = stateDir(), + ) { + this.info = { + pid: process.pid, + startedAt: Date.now(), + network: config.network, + ready: false, + }; + this.logger = new Logger({ + level: config.logLevel, + directory: join(directory, 'logs'), + }); + } + async start( + openSigner: () => Promise = async () => + privySigner(this.config, await privyClient(this.config)), + ) { + await mkdir(this.directory, { recursive: true, mode: 0o700 }); + await chmod(this.directory, 0o700); + const path = join(this.directory, 'runner.pid'); + const startupLock = path + '.startup'; + let guard; + try { + guard = await open(startupLock, 'wx', 0o600); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST') + throw new AgentCliError( + 'RUNNER_NOT_RUNNING', + 'Runner startup in progress; retry shortly', + ); + throw error; + } + let handle; + try { + const existing = await runningInfo(path); + if (existing) return { existing: true, ...existing }; + await unlink(path).catch((error) => { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + }); + handle = await open(path, 'wx', 0o600); + await handle.writeFile(JSON.stringify(this.info)); + await handle.close(); + } finally { + await guard.close(); + await unlink(startupLock); + } + try { + this.signer = await openSigner(); + this.timer = setInterval(() => {}, 30_000); + this.logger.log('info', 'Runner started', { ...this.info }); + await this.logger.flush(); + return { existing: false, ...this.info }; + } catch (error) { + await handle?.close().catch(() => {}); + await unlink(path).catch(() => {}); + throw error; + } + } + async markReady() { + this.info.ready = true; + await atomicWrite( + join(this.directory, 'runner.pid'), + JSON.stringify(this.info), + ); + } + onShutdown(cleanup: () => Promise) { + this.cleanup.push(cleanup); + } + async shutdown() { + if (this.closing) return; + this.closing = true; + if (this.timer) clearInterval(this.timer); + const errors: unknown[] = []; + for (const cleanup of this.cleanup.reverse()) + try { + await cleanup(); + } catch (error) { + errors.push(error); + } + this.signer = undefined; + this.logger.log('info', 'Runner stopped'); + await this.logger.flush(); + await unlink(join(this.directory, 'runner.pid')).catch((error) => { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + }); + if (activeRunner === this) activeRunner = undefined; + if (errors.length) + throw new AggregateError(errors, 'Runner cleanup failed'); + } + signals() { + const handler = () => { + void this.shutdown().catch((error) => { + process.stderr.write(String(error) + '\n'); + process.exitCode = 1; + }); + }; + process.once('SIGINT', handler); + process.once('SIGTERM', handler); + this.onShutdown(async () => { + process.off('SIGINT', handler); + process.off('SIGTERM', handler); + }); + } +} +export async function detachRunner(args: string[], path = runnerPidPath()) { + const existing = await runningInfo(path); + if (existing?.ready) return existing; + const compiled = + Boolean(process.versions.bun) && import.meta.url.includes('/$bunfs/'); + const child = spawn( + process.execPath, + compiled ? ['run', ...args] : [process.argv[1], 'run', ...args], + { detached: true, stdio: 'ignore', env: process.env }, + ); + child.unref(); + for (let i = 0; i < 600; i++) { + const info = await runningInfo(path); + if (info?.ready) return info; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + throw new AgentCliError( + 'RUNNER_NOT_RUNNING', + 'Detached runner did not become ready', + ); +} +export let activeRunner: Runner | undefined; +export function setActiveRunner(runner: Runner | undefined) { + activeRunner = runner; +} diff --git a/src/runner/rpc.ts b/src/runner/rpc.ts new file mode 100644 index 0000000..39eb289 --- /dev/null +++ b/src/runner/rpc.ts @@ -0,0 +1,314 @@ +import { assertSafeInput } from '../wallet/safety.js'; +import { + createServer, + createConnection, + type Server, + type Socket, +} from 'node:net'; +import { chmod, unlink, lstat } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { createHash } from 'node:crypto'; +import { z } from 'zod'; +import { stateDir, loadConfig } from '../core/config.js'; +import { AgentCliError, normalizeError } from '../errors/index.js'; +import { commands, type Context } from '../core/router.js'; +import { runningInfo, type Runner } from './lifecycle.js'; +export const socketPath = (directory = stateDir()) => + process.platform === 'win32' + ? `\\\\.\\pipe\\bloxwap-${createHash('sha256').update(directory).digest('hex').slice(0, 24)}` + : join(directory, 'run.sock'); +const aliases: Record = { + 'status.get': 'status', + 'market.get': 'market', + 'balance.get': 'balance', + 'positions.get': 'positions', + 'orders.get': 'orders', + 'order.place': 'order', + 'order.cancel': 'cancel', + 'order.modify': 'modify', + 'leverage.set': 'leverage', + 'config.get': 'config', + 'runner.shutdown': 'shutdown', +}; +export const routedCommands = new Set([ + 'market', + 'status', + 'balance', + 'positions', + 'orders', + 'order', + 'cancel', + 'modify', + 'leverage', + 'rotate', + 'revoke', +]); +const requestSchema = z.object({ + jsonrpc: z.literal('2.0'), + id: z.union([z.string(), z.number(), z.null()]), + method: z.string(), + params: z.record(z.string(), z.unknown()).default({}), +}); +export async function rpcServer( + runner: Runner, + context: Context, + execute?: (name: string, input: Record) => Promise, +): Promise { + const path = socketPath(runner.directory); + let queue = Promise.resolve(); + const sockets = new Set(); + let stopping = false; + const server = createServer((socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + let buffer = ''; + socket.setEncoding('utf8'); + socket.setTimeout(30_000, () => socket.destroy()); + socket.on('data', (chunk) => { + buffer += chunk; + if (Buffer.byteLength(buffer) > 1_048_576) { + socket.destroy(); + return; + } + let end: number; + while ((end = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, end); + buffer = buffer.slice(end + 1); + queue = queue + .then(async () => { + let id: unknown = null; + try { + if (stopping) + throw new AgentCliError( + 'RUNNER_NOT_RUNNING', + 'Runner stopping', + ); + const request = requestSchema.parse(JSON.parse(line)); + id = request.id; + const metadata = request.params.__context as + { configPath?: string; network?: string } | undefined; + if ( + metadata && + ((metadata.network && + metadata.network !== runner.config.network) || + (metadata.configPath && + resolve(metadata.configPath) !== + resolve( + context.configPath ?? join(stateDir(), 'config.json'), + ))) + ) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Runner belongs to a different config or network', + ); + const name = aliases[request.method] ?? request.method; + if (name === 'shutdown') { + socket.write( + JSON.stringify({ + jsonrpc: '2.0', + id, + result: { stopping: true }, + }) + '\n', + ); + setImmediate(() => { + void runner.shutdown(); + }); + return; + } + if (!routedCommands.has(name) && name !== 'config') + throw new AgentCliError( + 'CONFIG_INVALID', + 'Unknown runner method', + ); + const command = commands.get(name); + if (!command && !execute) + throw new AgentCliError('CONFIG_INVALID', 'Unavailable method'); + assertSafeInput(request.params); + const { __context, ...params } = request.params; + void __context; + const input = name === 'config' ? { args: ['get'] } : params; + const parsed = command?.schema.safeParse(input); + if (parsed && !parsed.success) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Invalid runner input', + ); + const data = execute + ? await execute(name, input) + : await command!.execute( + parsed!.data as Record, + { ...context, runner: true }, + ); + socket.write( + JSON.stringify({ jsonrpc: '2.0', id, result: data }) + '\n', + ); + } catch (error) { + const normalized = + error instanceof SyntaxError || error instanceof z.ZodError + ? new AgentCliError( + 'CONFIG_INVALID', + 'Invalid JSON-RPC request', + ) + : normalizeError(error); + socket.write( + JSON.stringify({ + jsonrpc: '2.0', + id, + error: { + code: -32000, + message: normalized.message, + data: normalized.toJSON(), + }, + }) + '\n', + ); + } + }) + .catch((error) => { + runner.logger.log('error', 'RPC write failed', { + error: String(error), + }); + socket.destroy(); + }); + } + }); + socket.on('error', () => {}); + }); + if (process.platform !== 'win32') { + try { + const stale = await lstat(path); + if (!stale.isSocket()) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Runner socket path contains a non-socket file', + ); + await unlink(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + } + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(path, () => { + server.off('error', reject); + resolve(); + }); + }); + if (process.platform !== 'win32') await chmod(path, 0o600); + runner.onShutdown(async () => { + stopping = true; + const closed = new Promise((resolve) => + server.close(() => resolve()), + ); + await queue; + for (const socket of sockets) socket.destroy(); + await closed; + if (process.platform !== 'win32') await unlink(path).catch(() => {}); + }); + return server; +} +export function rpcCall( + method: string, + params: Record = {}, + path = socketPath(), +): Promise { + return new Promise((resolve, reject) => { + const id = 1; + const socket = createConnection(path); + let buffer = ''; + socket.setEncoding('utf8'); + socket.setTimeout(15_000, () => + socket.destroy( + new AgentCliError( + 'NETWORK_ERROR', + 'Runner timed out; reconcile exchange state before retry', + ), + ), + ); + socket.once('connect', () => + socket.write( + JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n', + ), + ); + socket.on('data', (chunk) => { + buffer += chunk; + if (buffer.length > 1_048_576) { + socket.destroy(new Error('Oversize runner response')); + return; + } + if (!buffer.includes('\n')) return; + try { + const response = JSON.parse(buffer.slice(0, buffer.indexOf('\n'))); + if (response.jsonrpc !== '2.0' || response.id !== id) + throw new Error('Invalid runner response'); + if (response.error) { + const error = response.error.data; + reject( + new AgentCliError( + error.code, + error.message, + error.details, + error.retryable, + ), + ); + } else resolve(response.result); + socket.end(); + } catch (error) { + reject(error); + socket.destroy(); + } + }); + socket.once('error', (error) => + reject( + error instanceof AgentCliError + ? error + : new AgentCliError( + ['ENOENT', 'ECONNREFUSED'].includes( + (error as NodeJS.ErrnoException).code ?? '', + ) + ? 'RUNNER_NOT_RUNNING' + : 'NETWORK_ERROR', + error.message, + ), + ), + ); + socket.once('end', () => { + if (!buffer.includes('\n')) + reject( + new AgentCliError( + 'NETWORK_ERROR', + 'Runner disconnected; reconcile exchange state', + ), + ); + }); + }); +} +export async function routeToRunner( + name: string, + input: Record, + context: Context, +) { + if (context.runner || !routedCommands.has(name)) + return { routed: false as const }; + const config = await loadConfig(context.configPath); + try { + return { + routed: true as const, + data: await rpcCall(name, { + ...input, + __context: { + configPath: context.configPath ?? join(stateDir(), 'config.json'), + network: context.network ?? config.network, + }, + }), + }; + } catch (error) { + if ( + error instanceof AgentCliError && + error.code === 'RUNNER_NOT_RUNNING' && + config.runnerFallback && + !(await runningInfo()) + ) + return { routed: false as const }; + throw error; + } +} diff --git a/src/runner/subscriptions.ts b/src/runner/subscriptions.ts new file mode 100644 index 0000000..a9e0e30 --- /dev/null +++ b/src/runner/subscriptions.ts @@ -0,0 +1,178 @@ +import { SubscriptionClient, WebSocketTransport } from '@bloxwap/hyperliquid'; +import WebSocket from 'ws'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite, type Config } from '../core/config.js'; +import { hlInfo } from '../core/http.js'; +import { normalizeError } from '../errors/index.js'; +import type { Runner } from './lifecycle.js'; +interface Fill { + tid: number; + time: number; + [key: string]: unknown; +} +interface Backend { + subscribe: ( + type: string, + params: Record, + listener: (data: unknown) => void, + ) => Promise<{ unsubscribe: () => Promise }>; + onState: (listener: (state: string) => void) => void; + close: () => Promise; + gap: (startTime?: number) => Promise; +} +export function subscriptionSet(config: Config) { + return [ + 'userEvents', + 'userFills', + 'orderUpdates', + 'clearinghouseState', + ...(config.allowedMarkets.length + ? ['allMids', ...config.allowedMarkets.map((coin) => `l2Book:${coin}`)] + : []), + ]; +} +export class Subscriptions { + state = 'connecting'; + lastMessage = Date.now(); + lastFill: number | null = null; + private seen = new Set(); + private queue = Promise.resolve(); + private subscriptions: { unsubscribe: () => Promise }[] = []; + constructor( + private runner: Runner, + private backend: Backend, + private emit: (type: string, data: unknown) => Promise, + ) {} + async start() { + const path = join(this.runner.directory, 'subscriptions.json'); + try { + const saved = JSON.parse(await readFile(path, 'utf8')); + this.seen = new Set(saved.seen); + this.lastFill = saved.lastFill; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + this.backend.onState((state) => { + const reconnect = this.state !== 'connecting' && state === 'connected'; + this.state = state; + this.lastMessage = Date.now(); + if (reconnect) + this.enqueue(async () => { + for (const fill of await this.backend.gap( + this.lastFill ?? Date.now() - 60_000, + )) + await this.fill(fill); + await this.emit('runner_state', { wsState: state }); + }); + }); + for (const name of subscriptionSet(this.runner.config)) { + const [type, coin] = name.split(':'); + const params = coin + ? { coin } + : type === 'allMids' + ? {} + : { user: this.runner.config.masterAddress }; + this.subscriptions.push( + await this.backend.subscribe(type, params, (data) => { + this.lastMessage = Date.now(); + this.enqueue(async () => { + if (type === 'userFills') { + for (const fill of (data as { fills: Fill[] }).fills) + await this.fill(fill); + } else if (type === 'userEvents' && 'fills' in (data as object)) { + for (const fill of (data as { fills: Fill[] }).fills) + await this.fill(fill); + } else if (type === 'orderUpdates') + await this.emit('order_update', data); + else if (type === 'clearinghouseState') + await this.emit('position_change', data); + }); + }), + ); + } + this.runner.onShutdown(async () => { + await this.queue; + await Promise.all(this.subscriptions.map((s) => s.unsubscribe())); + await this.backend.close(); + }); + } + private enqueue(operation: () => Promise) { + this.queue = this.queue.then(operation).catch(async (error) => { + await this.emit('error', normalizeError(error).toJSON()); + }); + } + async fill(fill: Fill) { + if (this.seen.has(fill.tid)) return; + this.seen.add(fill.tid); + this.lastFill = Math.max(this.lastFill ?? 0, fill.time); + if (this.seen.size > 10000) + this.seen.delete(this.seen.values().next().value!); + await this.emit('fill', fill); + await atomicWrite( + join(this.runner.directory, 'subscriptions.json'), + JSON.stringify({ + seen: [...this.seen].slice(-10000), + lastFill: this.lastFill, + }), + ); + } + async flush() { + await this.queue; + } +} +export function sdkBackend(config: Config): Backend { + const transport = new WebSocketTransport({ + isTestnet: config.network === 'testnet', + resubscribe: true, + webSocketFactory: (url, protocols) => + new WebSocket(url, protocols) as unknown as globalThis.WebSocket, + }); + const client = new SubscriptionClient({ transport }) as unknown as Record< + string, + ( + params: Record, + listener: (data: unknown) => void, + ) => Promise<{ unsubscribe: () => Promise }> + >; + return { + subscribe: (type, params, listener) => client[type](params, listener), + onState: (listener) => + transport.events.addEventListener('connectionstatechange', (event) => + listener(event.detail), + ), + close: async () => { + await transport.close(); + }, + gap: async (startTime = Date.now() - 60_000) => { + const fills: Fill[] = []; + const endTime = Date.now(); + for (let page = 0; page < 100; page++) { + const batch = await hlInfo(config.network, 'userFillsByTime', { + user: config.masterAddress, + startTime, + endTime, + }); + fills.push(...batch); + if (batch.length < 2000) return fills; + const last = Math.max(...batch.map((fill) => fill.time)); + if (last <= startTime) + throw new Error('Fill backfill cannot advance; inspect gap'); + startTime = last; + } + throw new Error('Fill backfill exceeded pagination bound'); + }, + }; +} +export async function startSubscriptions( + runner: Runner, + emit: (type: string, data: unknown) => Promise, +) { + const subscriptions = new Subscriptions( + runner, + sdkBackend(runner.config), + emit, + ); + await subscriptions.start(); + return subscriptions; +} diff --git a/src/wallet/approval.ts b/src/wallet/approval.ts new file mode 100644 index 0000000..1bbff2c --- /dev/null +++ b/src/wallet/approval.ts @@ -0,0 +1,84 @@ +import { nextNonce } from '../core/nonce.js'; +import type { Config } from '../core/config.js'; +import { assertSafeInput } from './safety.js'; +import { ApproveAgentTypes } from '@bloxwap/hyperliquid/api/exchange/approveAgent'; +import { ApproveBuilderFeeTypes } from '@bloxwap/hyperliquid/api/exchange/approveBuilderFee'; +import { AgentCliError } from '../errors/index.js'; +import { jsonRequest, hlEndpoint } from '../core/http.js'; +export interface Approval { + action: Record; + typedData: Record; +} +export function approval( + network: 'mainnet' | 'testnet', + type: 'approveAgent' | 'approveBuilderFee', + fields: Record, + nonce = Date.now(), +): Approval { + const types = + type === 'approveAgent' ? ApproveAgentTypes : ApproveBuilderFeeTypes; + const action = { + type, + signatureChainId: '0xa4b1', + hyperliquidChain: network === 'mainnet' ? 'Mainnet' : 'Testnet', + ...fields, + nonce, + }; + const { type: _type, signatureChainId: _chain, ...message } = action; + void _type; + void _chain; + return { + action, + typedData: { + domain: { + name: 'HyperliquidSignTransaction', + version: '1', + chainId: 42161, + verifyingContract: '0x' + '0'.repeat(40), + }, + types, + primaryType: Object.keys(types)[0], + message, + }, + }; +} +export async function submitApproval( + network: 'mainnet' | 'testnet', + pending: Approval, + signature: string, +) { + if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) + throw new AgentCliError('CONFIG_INVALID', 'Expected 65-byte signature'); + const recovery = parseInt(signature.slice(-2), 16); + const v = recovery < 27 ? recovery + 27 : recovery; + if (v !== 27 && v !== 28) + throw new AgentCliError('CONFIG_INVALID', 'Invalid signature recovery'); + const response = (await jsonRequest(`${hlEndpoint(network)}/exchange`, { + action: pending.action, + nonce: pending.action.nonce, + signature: { + r: signature.slice(0, 66), + s: '0x' + signature.slice(66, 130), + v, + }, + })) as { status: string; response: unknown }; + if (response.status !== 'ok') + throw new AgentCliError('UPSTREAM_ERROR', 'Approval failed'); + return response; +} +export const rejectMasterKey = assertSafeInput; + +export async function newApproval( + config: Config, + type: 'approveAgent' | 'approveBuilderFee', + fields: Record, +) { + if (!config.masterAddress) + throw new AgentCliError('CONFIG_INVALID', 'Master address required'); + return approval( + config.network, + type, + fields, + await nextNonce(config.masterAddress), + ); +} diff --git a/src/wallet/expiry.ts b/src/wallet/expiry.ts new file mode 100644 index 0000000..f2e403c --- /dev/null +++ b/src/wallet/expiry.ts @@ -0,0 +1,50 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite, type Config, stateDir } from '../core/config.js'; +import { AgentCliError } from '../errors/index.js'; +import { events } from '../runner/events.js'; +export function assertAgentValid(config: Config, now = Date.now()) { + if (!config.agentExpiry || config.agentExpiry <= now) + throw new AgentCliError('AGENT_EXPIRED', 'Rotate expired agent'); +} +export async function expiryWarnings( + config: Config, + options: { + now?: number; + path?: string; + emit?: (data: unknown) => Promise; + } = {}, +) { + if (!config.agentExpiry) return []; + const now = options.now ?? Date.now(); + const path = options.path ?? join(stateDir(), 'expiry-warnings.json'); + let emitted: string[] = []; + try { + emitted = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + const days = (config.agentExpiry - now) / 86400000; + const warnings = []; + for (const threshold of config.expiryWarningDays) { + const key = `${config.agentAddress}:${config.agentExpiry}:${threshold}`; + if (days <= threshold && !emitted.includes(key)) { + const warning = { + agentAddress: config.agentAddress, + expiry: config.agentExpiry, + daysRemaining: String(Math.max(0, days)), + threshold: String(threshold), + }; + await ( + options.emit ?? + (async (data) => { + await events.append('agent_expiry_warning', data); + }) + )(warning); + emitted.push(key); + warnings.push(warning); + } + } + if (warnings.length) await atomicWrite(path, JSON.stringify(emitted)); + return warnings; +} diff --git a/src/wallet/policy.ts b/src/wallet/policy.ts new file mode 100644 index 0000000..7940383 --- /dev/null +++ b/src/wallet/policy.ts @@ -0,0 +1,51 @@ +import type { PrivyClient } from '@privy-io/node'; +import template from '../../policies/agent-trade-only.json' with { type: 'json' }; +import { AgentCliError } from '../errors/index.js'; +import { mapPrivyError } from '../errors/mappers.js'; +import { saveConfig, type Config } from '../core/config.js'; +export const policyVersion = 1; +const allowed = new Set([ + 'order', + 'cancel', + 'cancelByCloid', + 'batchModify', + 'modify', + 'updateLeverage', + 'scheduleCancel', + 'noop', +]); +export function assertTradeAction(action: string) { + if (!allowed.has(action)) + throw new AgentCliError('POLICY_REJECTED', `Agent cannot sign ${action}`); +} +export function policyFor(network: Config['network']) { + const policy = structuredClone(template); + policy.rules[0].conditions[2].value = network === 'mainnet' ? 'a' : 'b'; + return policy; +} +export async function applyPolicy( + config: Config, + client: PrivyClient, + path?: string, +) { + if (config.policyId) return config.policyId; + try { + const policy = await client + .policies() + .create( + policyFor(config.network) as Parameters< + ReturnType['create'] + >[0], + ); + if (config.privyWalletId) + await client + .wallets() + .update(config.privyWalletId, { policy_ids: [policy.id] }); + config.policyId = policy.id; + config.policyVersion = policyVersion; + await saveConfig(config, path); + return policy.id; + } catch (error) { + throw mapPrivyError(error); + } +} diff --git a/src/wallet/privy.ts b/src/wallet/privy.ts new file mode 100644 index 0000000..c9a8535 --- /dev/null +++ b/src/wallet/privy.ts @@ -0,0 +1,131 @@ +import { signatureAudit } from '../core/audit.js'; +import { PrivyClient, generateP256KeyPair } from '@privy-io/node'; +import { randomUUID, createHash } from 'node:crypto'; +import { readFile, stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { + atomicWrite, + saveConfig, + stateDir, + type Config, +} from '../core/config.js'; +import type { Signer } from '../core/hlclient.js'; +import { AgentCliError } from '../errors/index.js'; +import { mapPrivyError } from '../errors/mappers.js'; +export async function resolveSecret( + ref: string | undefined, + env = process.env, +): Promise { + if (!ref) + throw new AgentCliError('CONFIG_INVALID', 'Missing secret reference'); + if (ref.startsWith('env:')) { + const value = env[ref.slice(4)]; + if (value) return value; + } + if (ref.startsWith('file:')) { + const path = ref.slice(5); + const info = await stat(path); + if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Secret file must have mode 0600', + ); + return (await readFile(path, 'utf8')).trim(); + } + throw new AgentCliError('CONFIG_INVALID', 'Secret unavailable'); +} +export async function privyClient(config: Config) { + if (!config.privyAppId) + throw new AgentCliError('CONFIG_INVALID', 'Set privyAppId'); + return new PrivyClient({ + appId: config.privyAppId, + appSecret: await resolveSecret( + config.privyAppSecretRef ?? 'env:BLOXWAP_PRIVY_APP_SECRET', + ), + }); +} +export async function createAgent( + config: Config, + client: PrivyClient, + path?: string, + policyId?: string, +) { + try { + if (config.privyWalletId) { + const wallet = await client.wallets().get(config.privyWalletId); + if (!config.authorizationKeyRef) { + const pair = await generateP256KeyPair(); + const keyPath = join(stateDir(), `authorization-${randomUUID()}.key`); + await atomicWrite(keyPath, pair.privateKey); + await client + .wallets() + .update(wallet.id, { owner: { public_key: pair.publicKey } }); + config.authorizationKeyRef = `file:${keyPath}`; + } + config.agentAddress = wallet.address; + await saveConfig(config, path); + return wallet; + } + const pair = await generateP256KeyPair(); + const keyPath = join(stateDir(), `authorization-${randomUUID()}.key`); + await atomicWrite(keyPath, pair.privateKey); + const wallet = await client + .wallets() + .create({ + chain_type: 'ethereum', + owner: { public_key: pair.publicKey }, + policy_ids: policyId ? [policyId] : [], + idempotency_key: `agent-${createHash('sha256').update(pair.publicKey).digest('hex')}`, + }); + config.privyWalletId = wallet.id; + config.agentAddress = wallet.address; + config.authorizationKeyRef = `file:${keyPath}`; + await saveConfig(config, path); + return wallet; + } catch (error) { + throw mapPrivyError(error); + } +} +export async function privySigner( + config: Config, + client: PrivyClient, +): Promise { + if (!config.agentAddress || !config.privyWalletId) + throw new AgentCliError( + 'AGENT_NOT_APPROVED', + 'Initialize agent wallet first', + ); + const authorizationKey = await resolveSecret(config.authorizationKeyRef); + return { + address: config.agentAddress as `0x${string}`, + signTypedData: async (data) => { + try { + const signed = await client + .wallets() + .ethereum() + .signTypedData(config.privyWalletId!, { + params: { + typed_data: { + domain: data.domain, + types: Object.fromEntries( + Object.entries(data.types).map(([key, fields]) => [ + key, + [...fields], + ]), + ), + primary_type: data.primaryType, + message: data.message, + }, + }, + authorization_context: { + authorization_private_keys: [authorizationKey], + }, + }); + await signatureAudit(signed.signature); + return signed.signature as `0x${string}`; + } catch (error) { + throw mapPrivyError(error); + } + }, + }; +} diff --git a/src/wallet/safety.ts b/src/wallet/safety.ts new file mode 100644 index 0000000..dc0ff86 --- /dev/null +++ b/src/wallet/safety.ts @@ -0,0 +1,38 @@ +import { wordlist } from '@scure/bip39/wordlists/english.js'; +import { AgentCliError } from '../errors/index.js'; +const words = new Set(wordlist); +export function resemblesSecret(value: string) { + const trimmed = value.trim(); + if ( + /(?:^|[^0-9a-fA-F])(?:0x)?[0-9a-fA-F]{64}(?=$|[^0-9a-fA-F])/.test(trimmed) + ) + return true; + const tokens = trimmed + .toLowerCase() + .split(/\s+/) + .map((token) => token.replace(/^[^a-z]+|[^a-z]+$/g, '')); + for (let start = 0; start + 12 <= tokens.length; start++) + if (tokens.slice(start, start + 12).every((word) => words.has(word))) + return true; + return ( + [12, 15, 18, 21, 24].includes(tokens.length) && + tokens.every((word) => words.has(word)) + ); +} +export function assertSafeInput(value: unknown): void { + if (typeof value === 'string' && resemblesSecret(value)) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Private keys and seed phrases are forbidden; use external wallet signing', + ); + if (Array.isArray(value)) value.forEach(assertSafeInput); + else if (value && typeof value === 'object') + for (const [key, field] of Object.entries(value)) { + if (/private.?key|seed.?phrase|mnemonic/i.test(key)) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Secret key input is forbidden', + ); + assertSafeInput(field); + } +} diff --git a/test/__snapshots__/qr.test.ts.snap b/test/__snapshots__/qr.test.ts.snap new file mode 100644 index 0000000..6fe5140 --- /dev/null +++ b/test/__snapshots__/qr.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`QR deterministic terminal snapshot stays off stdout 1`] = ` +" ▄▄▄▄▄▄▄ ▄▄ ▄▄▄▄▄▄▄  + █ ▄▄▄ █ ▄▄▀▄▄ █ ▄▄▄ █  + █ ███ █ █ █▄ █ ███ █  + █▄▄▄▄▄█ █ ▄ █ █▄▄▄▄▄█  + ▄ ▄▄▄▄▄ ▀▀ ▀█ ▄▄▄▄▄  + █▀▄▄▀█▄█ █▄▀ █▄▀▀▄▄▀  + ▀ █▄█▄▄▄ ▄▀▄ █▄█ ▀▀ ▀  + ▄▄▄▄▄▄▄ ▀ █ █▀ ▀█ ▄▀  + █ ▄▄▄ █ █ ▄▄█▀▄▄ █▀▄▄  + █ ███ █ █▄▄█▄█ ▀▀ ▄  + █▄▄▄▄▄█ ▄ ▀▀▄█▄ ▀█▀▄  +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +" +`; diff --git a/test/amounts.test.ts b/test/amounts.test.ts new file mode 100644 index 0000000..e14922c --- /dev/null +++ b/test/amounts.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from 'vitest'; +import { + parseUsdc, + formatUsdc, + toHyperCore, + fromHyperCore, +} from '../src/funding/amounts.js'; +test('exact bigint round trips across decimals and large values', () => { + for (const decimals of [0, 6, 8]) + for (let i = -1000n; i < 1000n; i += 7n) { + const value = i * 123456789012345678901n; + expect(parseUsdc(formatUsdc(value, decimals), decimals)).toBe(value); + } + expect(fromHyperCore(toHyperCore(1234567n))).toBe(1234567n); + expect(() => fromHyperCore(101n)).toThrow(); + expect(() => parseUsdc('1.0000001', 6)).toThrow(); +}); diff --git a/test/approval.test.ts b/test/approval.test.ts new file mode 100644 index 0000000..508d5e8 --- /dev/null +++ b/test/approval.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from 'vitest'; +import { approval, rejectMasterKey } from '../src/wallet/approval.js'; +test('user signature payload follows SDK types and master secrets are refused', () => { + const result = approval( + 'testnet', + 'approveAgent', + { agentAddress: '0x' + 'a'.repeat(40), agentName: 'agent' }, + 123, + ); + expect(result.typedData).toMatchObject({ + primaryType: 'HyperliquidTransaction:ApproveAgent', + message: { nonce: 123, hyperliquidChain: 'Testnet' }, + }); + expect(() => rejectMasterKey({ key: '0x' + 'ab'.repeat(32) })).toThrow(); + expect(() => rejectMasterKey({ privateKey: 'anything' })).toThrow(); + rejectMasterKey({ signature: '0x' + 'ab'.repeat(65) }); +}); diff --git a/test/audit.test.ts b/test/audit.test.ts new file mode 100644 index 0000000..39053e8 --- /dev/null +++ b/test/audit.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { AuditStore, readAudit, verifyAudit } from '../src/core/audit.js'; +test('audit chain detects edits and excludes sensitive signatures', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-audit-')); + const path = join(dir, 'audit.jsonl'); + try { + const store = new AuditStore(path); + await store.append( + 'intent', + { signature: 'secret' }, + { intentId: 'one', cloid: 'client' }, + ); + await store.append('decision', { allowed: true }, { intentId: 'one' }); + const entries = await readAudit(path); + expect(verifyAudit(entries)).toBe(true); + expect(JSON.stringify(entries)).not.toContain('secret'); + entries[0].data = 'tampered'; + expect(() => verifyAudit(entries)).toThrow(); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/builder.test.ts b/test/builder.test.ts new file mode 100644 index 0000000..d5a3c3d --- /dev/null +++ b/test/builder.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest'; +import { approvedBuilder } from '../src/core/builder.js'; +import { migrate } from '../src/core/config.js'; +test('builder fee cannot exceed master approval', async () => { + const config = migrate({ + masterAddress: '0x' + 'a'.repeat(40), + builder: { address: '0x' + 'b'.repeat(40), fee: 10 }, + }); + expect(await approvedBuilder(config, async () => 10)).toEqual({ + b: config.builder.address, + f: 10, + }); + await expect(approvedBuilder(config, async () => 9)).rejects.toMatchObject({ + code: 'AGENT_NOT_APPROVED', + }); +}); diff --git a/test/cancel.test.ts b/test/cancel.test.ts new file mode 100644 index 0000000..0dbb1e1 --- /dev/null +++ b/test/cancel.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from 'vitest'; +import { cancelOrders, cancelSchema } from '../src/commands/cancel.js'; +import { mockTrading } from './fixtures.js'; +import { migrate } from '../src/core/config.js'; +test('batch cancellation selects requested orders and rejects unknown ids', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { coin: 'BTC', oid: 1, sz: '1', limitPx: '2', side: 'B' }, + { coin: 'BTC', oid: 2, sz: '1', limitPx: '2', side: 'B' }, + ]; + expect( + (await cancelOrders(config, cancelSchema.parse({ oids: [1, 2] }), services)) + .cancelled, + ).toEqual(['1', '2']); + await expect( + cancelOrders(config, cancelSchema.parse({ oid: 9 }), services), + ).rejects.toMatchObject({ code: 'ORDER_NOT_FOUND' }); +}); diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..34833a8 --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,64 @@ +import '../src/commands/config.js'; +import { dispatch } from '../src/core/router.js'; +import { mkdtemp, stat, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { expect, test } from 'vitest'; +import { loadConfig, saveConfig, migrate } from '../src/core/config.js'; +test('atomic private store, migration and corrupt input', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-')); + const path = join(dir, 'state', 'config.json'); + try { + await saveConfig({ network: 'testnet' }, path); + expect((await stat(path)).mode & 0o777).toBe(0o600); + expect((await stat(join(dir, 'state'))).mode & 0o777).toBe(0o700); + expect( + (await loadConfig(path, { BLOXWAP_NETWORK: 'mainnet' })).network, + ).toBe('mainnet'); + await writeFile(path, 'broken'); + await expect(loadConfig(path, {})).rejects.toMatchObject({ + code: 'CONFIG_INVALID', + }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test('builder defaults, persisted overrides and validated environment precedence', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-builder-')); + const path = join(dir, 'config.json'); + const address = '0x' + 'b'.repeat(40); + try { + expect((await loadConfig(path, {})).builder).toEqual({ + address: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + fee: 10, + }); + expect(migrate({ builder: {} }).builder.fee).toBe(10); + for (const [key, value] of [ + ['builder.address', address], + ['builder.fee', '0'], + ]) { + expect( + (await dispatch(['config', 'set', key, value, '--config', path])).body, + ).toMatchObject({ ok: true }); + } + expect((await loadConfig(path, {})).builder).toEqual({ address, fee: 0 }); + expect( + ( + await loadConfig(path, { + BLOXWAP_BUILDER_ADDRESS: '0x' + 'c'.repeat(40), + BLOXWAP_BUILDER_FEE: '20', + }) + ).builder, + ).toEqual({ address: '0x' + 'c'.repeat(40), fee: 20 }); + for (const fee of ['bad', '', '-1', '101', '1.5']) + await expect( + loadConfig(path, { BLOXWAP_BUILDER_FEE: fee }), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + await expect( + loadConfig(path, { BLOXWAP_BUILDER_ADDRESS: 'bad' }), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/daily-loss.test.ts b/test/daily-loss.test.ts new file mode 100644 index 0000000..ef69417 --- /dev/null +++ b/test/daily-loss.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest'; +import { dailyLossFromPortfolio } from '../src/core/trading.js'; +test('daily loss uses realized and marked pnl history across UTC day', () => { + const midnight = Date.UTC(2026, 0, 1); + expect( + dailyLossFromPortfolio( + [ + [ + 'day', + { + pnlHistory: [ + [midnight - 1000, '100'], + [midnight + 1000, '90.12345678'], + ], + }, + ], + ], + midnight + 2000, + ), + ).toBe('9.87654322'); + expect(() => dailyLossFromPortfolio([])).toThrow(); +}); diff --git a/test/deposit-signatures.test.ts b/test/deposit-signatures.test.ts new file mode 100644 index 0000000..731dc63 --- /dev/null +++ b/test/deposit-signatures.test.ts @@ -0,0 +1,115 @@ +import { expect, test, vi } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + writeDeposits, + type Deposit, + readDeposits, +} from '../src/funding/store.js'; +import { + signatureSteps, + submitDepositSignature, +} from '../src/funding/signatures.js'; +import { quoteFixture } from './fixtures.js'; +test('signature steps are ordered, typed and resumable without replaying completed steps', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-signatures-')); + const path = join(dir, 'deposits.json'); + const quote: Deposit['quote'] = structuredClone(quoteFixture); + quote.steps = [ + { + id: 'first', + kind: 'signature', + requestId: 'deposit', + depositAddress: '0x' + 'a'.repeat(40), + items: [ + { + status: 'incomplete', + data: { + sign: { + signatureKind: 'eip712', + domain: { name: 'Relay' }, + types: { Mapping: [] }, + primaryType: 'Mapping', + value: { nonce: 1 }, + }, + post: { + method: 'POST', + endpoint: '/authorize', + body: { nonce: 1 }, + }, + }, + }, + ], + }, + { + id: 'second', + kind: 'signature', + requestId: 'deposit', + depositAddress: '0x' + 'a'.repeat(40), + items: [ + { + status: 'incomplete', + data: { + sign: { message: 'second' }, + post: { + method: 'POST', + endpoint: '/authorize', + body: { nonce: 2 }, + }, + }, + }, + ], + }, + ]; + const deposit: Deposit = { + id: 'deposit', + network: 'testnet', + recipient: '0x' + 'a'.repeat(40), + route: 'route', + quote, + expiresAt: Date.now() + 10000, + state: 'awaiting_funds', + baseline: '0', + createdAt: Date.now(), + completedSteps: [], + }; + const submit = vi.fn(async () => ({ ok: true })); + const signature = '0x' + '11'.repeat(64) + '1b'; + try { + await writeDeposits([deposit], path); + expect(signatureSteps(deposit)[0].payload).toMatchObject({ + message: { nonce: 1 }, + primaryType: 'Mapping', + }); + await expect( + submitDepositSignature('deposit', 'second:0', signature, { + path, + submit, + }), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + expect(submit).not.toHaveBeenCalled(); + await submitDepositSignature('deposit', 'first:0', signature, { + path, + submit, + }); + expect((await readDeposits(path))[0].completedSteps).toEqual(['first:0']); + await submitDepositSignature('deposit', 'second:0', signature, { + path, + submit, + }); + expect( + await submitDepositSignature('deposit', 'second:0', signature, { + path, + submit, + }), + ).toEqual({ complete: true }); + expect(submit).toHaveBeenCalledTimes(2); + expect(submit.mock.calls[0]).toEqual([ + `https://api.testnets.relay.link/authorize?signature=${signature}`, + { nonce: 1 }, + ]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/deposit-status.test.ts b/test/deposit-status.test.ts new file mode 100644 index 0000000..2606715 --- /dev/null +++ b/test/deposit-status.test.ts @@ -0,0 +1,39 @@ +import { expect, test } from 'vitest'; +import { advanceDeposit } from '../src/funding/status.js'; +import type { Deposit } from '../src/funding/store.js'; +import { quoteFixture } from './fixtures.js'; +test('settlement requires Relay success plus expected balance increase', () => { + const deposit = { + quote: quoteFixture, + state: 'awaiting_funds', + baseline: '2', + id: 'one', + network: 'testnet', + recipient: 'master', + route: 'route', + expiresAt: 2000000000000, + createdAt: 0, + completedSteps: [], + } satisfies Deposit; + expect(advanceDeposit(deposit, 'success', '2.9')).toBe('pending'); + expect(advanceDeposit(deposit, 'success', '3')).toBe('settled'); + expect(advanceDeposit(deposit, 'failed', '3')).toBe('failed'); + expect(advanceDeposit({ ...deposit, state: 'settled' }, 'failed', '0')).toBe( + 'settled', + ); +}); +test('marked PnL changes do not count as a deposit receipt', async () => { + const { collateralBalance } = await import('../src/core/account.js'); + expect( + collateralBalance({ + marginSummary: { accountValue: '110' }, + assetPositions: [{ position: { unrealizedPnl: '60' } }], + }), + ).toBe('50'); + expect( + collateralBalance({ + marginSummary: { accountValue: '90' }, + assetPositions: [{ position: { unrealizedPnl: '-10' } }], + }), + ).toBe('100'); +}); diff --git a/test/errors.test.ts b/test/errors.test.ts new file mode 100644 index 0000000..aaa6705 --- /dev/null +++ b/test/errors.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from 'vitest'; +import { AgentCliError, errorDefaults, success } from '../src/errors/index.js'; +const original = [ + 'INSUFFICIENT_MARGIN', + 'POLICY_REJECTED', + 'AGENT_EXPIRED', + 'AGENT_NOT_APPROVED', + 'DEPOSIT_PENDING', + 'NONCE_COLLISION', + 'RATE_LIMIT', + 'INVALID_SIZE', + 'INVALID_PRICE', + 'UNKNOWN_MARKET', + 'ORDER_NOT_FOUND', + 'RUNNER_NOT_RUNNING', + 'CONFIG_INVALID', + 'NETWORK_ERROR', + 'UPSTREAM_ERROR', + 'INTERNAL', +]; +test('catalog is append-only', () => + original.forEach((code) => expect(errorDefaults).toHaveProperty(code))); +test('stable wire shapes', () => { + expect(new AgentCliError('RATE_LIMIT', 'wait').toJSON()).toEqual({ + ok: false, + code: 'RATE_LIMIT', + message: 'wait', + retryable: true, + details: null, + }); + expect(success('yes')).toEqual({ ok: true, data: 'yes' }); +}); diff --git a/test/events.test.ts b/test/events.test.ts new file mode 100644 index 0000000..9125046 --- /dev/null +++ b/test/events.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { EventStore, readEvents } from '../src/runner/events.js'; +test('event replay preserves monotonic seq across restart and filters', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-events-')); + const path = join(dir, 'events.jsonl'); + try { + const store = new EventStore(path); + await Promise.all([ + store.append('fill', { tid: 1 }), + store.append('error', { ok: false, code: 'NETWORK_ERROR' }), + ]); + await new EventStore(path).append('deposit', { id: 'deposit' }); + expect((await readEvents(path)).map((e) => e.seq)).toEqual([1, 2, 3]); + expect(await readEvents(path, 1, ['deposit'])).toHaveLength(1); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); +test('independent event writers serialize sequences and expose exact fee units', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-events-parallel-')); + const path = join(dir, 'events.jsonl'); + try { + await Promise.all([ + new EventStore(path).append('fill', { tid: 1, fee: '0.00000001' }), + new EventStore(path).append('deposit', { id: 'one' }), + ]); + const entries = await readEvents(path); + expect(entries.map((e) => e.seq)).toEqual([1, 2]); + expect(entries.find((e) => e.type === 'fill')?.data).toMatchObject({ + feeUnits: '1', + }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/expiry.test.ts b/test/expiry.test.ts new file mode 100644 index 0000000..8010e69 --- /dev/null +++ b/test/expiry.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { assertAgentValid, expiryWarnings } from '../src/wallet/expiry.js'; +import { migrate } from '../src/core/config.js'; +test('mocked clock blocks expiry and emits each threshold once', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-expiry-')); + const config = migrate({ agentExpiry: 15 * 86400000 }); + const data: unknown[] = []; + try { + const options = { + path: join(dir, 'warnings.json'), + now: 12 * 86400000, + emit: async (value: unknown) => { + data.push(value); + }, + }; + expect(await expiryWarnings(config, options)).toHaveLength(2); + expect(await expiryWarnings(config, options)).toHaveLength(0); + assertAgentValid(config, 14 * 86400000); + expect(() => assertAgentValid(config, 15 * 86400000)).toThrow( + expect.objectContaining({ code: 'AGENT_EXPIRED', retryable: false }), + ); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/fixtures.ts b/test/fixtures.ts new file mode 100644 index 0000000..e6626c4 --- /dev/null +++ b/test/fixtures.ts @@ -0,0 +1,64 @@ +import { vi } from 'vitest'; +import type { TradingServices } from '../src/core/trading.js'; +import type { AccountState } from '../src/commands/read.js'; +export const accountFixture: AccountState = { + marginSummary: { + accountValue: '100.12345678', + totalMarginUsed: '4', + totalNtlPos: '20', + }, + withdrawable: '96', + assetPositions: [ + { + position: { + coin: 'BTC', + szi: '0.001', + entryPx: '20000', + unrealizedPnl: '1', + liquidationPx: '10000', + leverage: { type: 'cross', value: 5 }, + }, + }, + ], +}; +export function mockTrading(): TradingServices { + return { + market: async () => ({ + name: 'BTC', + szDecimals: 5, + maxLeverage: 40, + asset: 0, + }), + state: async () => accountFixture, + orders: async () => [], + mids: async () => ({ BTC: '20000' }), + exchange: { + order: vi.fn(async () => ({ status: 'ok' })), + cancel: vi.fn(async () => ({ status: 'ok' })), + cancelByCloid: vi.fn(async () => ({ status: 'ok' })), + batchModify: vi.fn(async () => ({ status: 'ok' })), + updateLeverage: vi.fn(async () => ({ status: 'ok' })), + }, + }; +} +export const quoteFixture = { + steps: [ + { + id: 'deposit', + kind: 'transaction', + requestId: 'deposit-1', + depositAddress: '0x' + 'a'.repeat(40), + items: [{ status: 'incomplete', data: { chainId: 42161 } }], + }, + ], + fees: { + app: { amount: '10' }, + gas: { amount: '20' }, + relayerService: { amount: '30' }, + }, + details: { + currencyOut: { amount: '100000000', currency: { decimals: 8 } }, + minAmount: '1000000', + expiresAt: 2000000000000, + }, +}; diff --git a/test/fund.test.ts b/test/fund.test.ts new file mode 100644 index 0000000..8afb1d9 --- /dev/null +++ b/test/fund.test.ts @@ -0,0 +1,37 @@ +import { expect, test, vi } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fund, fundSchema } from '../src/commands/fund.js'; +import { migrate } from '../src/core/config.js'; +import { RelayClient } from '../src/funding/relay.js'; +import { quoteFixture } from './fixtures.js'; +test('fund persists and reuses unexpired quotes', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-fund-')); + const quote = vi.fn(async () => quoteFixture); + const config = migrate({ masterAddress: '0x' + 'a'.repeat(40) }); + try { + const dependencies = { + path: join(dir, 'deposits.json'), + relay: { quote } as unknown as RelayClient, + baseline: async () => '0', + qr: async () => ({ qrAscii: null, qrPath: null }), + }; + const input = fundSchema.parse({ amount: '10', qr: false }); + const first = await fund(config, input, dependencies); + expect(first).toMatchObject({ + builderFeeRate: '0.01%', + builderAddress: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + }); + expect(first.expectedReceived).toBe('100000000'); + expect((await fund(config, input, dependencies)).depositAddress).toBe( + first.depositAddress, + ); + expect(quote).toHaveBeenCalledTimes(1); + await expect( + fund(config, { ...input, amount: '0.1' }, dependencies), + ).rejects.toMatchObject({ code: 'INVALID_SIZE' }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/health.test.ts b/test/health.test.ts new file mode 100644 index 0000000..a143ac2 --- /dev/null +++ b/test/health.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest'; +import { health } from '../src/runner/health.js'; +import { Runner } from '../src/runner/lifecycle.js'; +import { migrate } from '../src/core/config.js'; +import type { Subscriptions } from '../src/runner/subscriptions.js'; +test('health reports staleness, uptime and signer state', () => { + const runner = new Runner(migrate({})); + runner.subscriptions = { + state: 'connected', + lastMessage: 1000, + lastFill: 500, + } as Subscriptions; + expect(health(runner, 2000)).toMatchObject({ + wsState: 'connected', + wsStale: false, + lastFillTime: 500, + signingSession: 'unavailable', + }); + expect(health(runner, 100000).wsStale).toBe(true); +}); diff --git a/test/hlclient.test.ts b/test/hlclient.test.ts new file mode 100644 index 0000000..0a0cf9b --- /dev/null +++ b/test/hlclient.test.ts @@ -0,0 +1,40 @@ +import { expect, test, vi } from 'vitest'; +import { + MetaCache, + roundPrice, + roundSize, + createClients, +} from '../src/core/hlclient.js'; +test('fixture metadata caches and refreshes', async () => { + const fixture = { + universe: [{ name: 'BTC', szDecimals: 5, maxLeverage: 40 }], + }; + const fetch = vi.fn(async () => fixture); + const cache = new MetaCache(fetch); + expect(await cache.lookup('BTC')).toEqual({ + ...fixture.universe[0], + asset: 0, + }); + await cache.lookup('BTC'); + expect(fetch).toHaveBeenCalledTimes(1); + await cache.refresh(); + expect(fetch).toHaveBeenCalledTimes(2); + await expect(cache.lookup('NO')).rejects.toMatchObject({ + code: 'UNKNOWN_MARKET', + }); +}); +test('SDK rounds exact decimal fixtures', () => { + expect(roundSize('1.234567', 5)).toBe('1.23456'); + expect(roundPrice('97123.456', 5)).toBe('97123'); + expect(roundPrice('1.23456', 3)).toBe('1.234'); + expect(() => roundSize('0.000001', 5)).toThrow(); + expect(createClients('testnet').exchange).toBeUndefined(); +}); +test('malformed metadata cannot disable leverage or precision limits', async () => { + const cache = new MetaCache(async () => ({ + universe: [{ name: 'BTC', szDecimals: 5, maxLeverage: NaN }], + })); + await expect(cache.lookup('BTC')).rejects.toMatchObject({ + code: 'UPSTREAM_ERROR', + }); +}); diff --git a/test/leverage.test.ts b/test/leverage.test.ts new file mode 100644 index 0000000..8f5d9f1 --- /dev/null +++ b/test/leverage.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from 'vitest'; +import { setLeverage, leverageSchema } from '../src/commands/leverage.js'; +import { mockTrading } from './fixtures.js'; +import { migrate } from '../src/core/config.js'; +test('cross/isolated leverage enforces both ceilings', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + limits: { maxLeverage: 5 }, + }); + const services = mockTrading(); + await setLeverage( + config, + leverageSchema.parse({ coin: 'BTC', leverage: 5, mode: 'isolated' }), + services, + ); + expect(services.exchange.updateLeverage).toHaveBeenCalledWith({ + asset: 0, + isCross: false, + leverage: 5, + }); + await expect( + setLeverage( + config, + leverageSchema.parse({ coin: 'BTC', leverage: 6 }), + services, + ), + ).rejects.toMatchObject({ code: 'POLICY_REJECTED' }); +}); +test('leverage updates respect configured daily loss', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + limits: { dailyLoss: 5 }, + }); + const services = mockTrading(); + services.dailyLoss = async () => '6'; + await expect( + setLeverage( + config, + leverageSchema.parse({ coin: 'BTC', leverage: 2 }), + services, + ), + ).rejects.toMatchObject({ code: 'POLICY_REJECTED' }); +}); diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts new file mode 100644 index 0000000..4fea959 --- /dev/null +++ b/test/lifecycle.test.ts @@ -0,0 +1,293 @@ +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; +import { mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +const environment = vi.hoisted(() => ({ + home: `/tmp/bloxwap-e2e-${process.pid}-${Math.random().toString(16).slice(2)}`, + wallets: 0, + orders: [] as { + coin: string; + oid: number; + side: string; + sz: string; + limitPx: string; + }[], + agents: [] as { address: string; validUntil: number }[], + builderFee: 0, + balance: '0', +})); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + homedir: () => environment.home, +})); +vi.mock('../src/wallet/privy.js', async (importOriginal) => { + const original = + await importOriginal(); + return { + ...original, + privyClient: async () => ({ + wallets: () => ({ + ethereum: () => ({ + signTypedData: async () => ({ + signature: '0x' + '11'.repeat(64) + '1b', + }), + }), + }), + }), + createAgent: async ( + config: import('../src/core/config.js').Config, + _client: unknown, + path?: string, + ) => { + const { saveConfig } = await import('../src/core/config.js'); + if (config.agentAddress && config.privyWalletId) + return { id: config.privyWalletId, address: config.agentAddress }; + config.agentAddress = '0x' + String(++environment.wallets).repeat(40); + config.privyWalletId = 'wallet-' + environment.wallets; + config.authorizationKeyRef = 'env:BLOXWAP_E2E_AUTH'; + await saveConfig(config, path); + return { id: config.privyWalletId, address: config.agentAddress }; + }, + privySigner: original.privySigner, + }; +}); +vi.mock('../src/wallet/policy.js', async (importOriginal) => ({ + ...(await importOriginal()), + applyPolicy: async (config: import('../src/core/config.js').Config) => { + config.policyId = 'policy'; + config.policyVersion = 1; + return 'policy'; + }, +})); +vi.mock('../src/runner/subscriptions.js', async (importOriginal) => ({ + ...(await importOriginal()), + startSubscriptions: async () => ({ + state: 'connected', + lastMessage: Date.now(), + lastFill: null, + }), +})); +import '../src/commands/init.js'; +import '../src/commands/fund.js'; +import '../src/commands/read.js'; +import '../src/commands/order.js'; +import '../src/commands/cancel.js'; +import '../src/commands/modify.js'; +import '../src/commands/leverage.js'; +import '../src/commands/run.js'; +import '../src/commands/rotate.js'; +import '../src/commands/revoke.js'; +import '../src/commands/events.js'; +import '../src/commands/audit.js'; +import '../src/commands/market.js'; +import { dispatch } from '../src/core/router.js'; +import { saveConfig, migrate } from '../src/core/config.js'; +import { activeRunner, setActiveRunner } from '../src/runner/lifecycle.js'; +import { rpcCall, socketPath } from '../src/runner/rpc.js'; +import { readAudit, verifyAudit } from '../src/core/audit.js'; +let path: string; +beforeEach(async () => { + await mkdir(environment.home, { recursive: true, mode: 0o700 }); + path = join(environment.home, '.bloxwap', 'config.json'); + process.env.BLOXWAP_E2E_AUTH = 'mock-authorization'; + environment.wallets = 0; + environment.orders = []; + environment.agents = []; + environment.balance = '0'; + environment.builderFee = 0; + await saveConfig( + migrate({ privyAppId: 'mock', masterAddress: '0x' + 'a'.repeat(40) }), + path, + ); + vi.stubGlobal( + 'fetch', + vi.fn(async (url: unknown, options?: RequestInit) => { + const body = JSON.parse(String(options?.body ?? '{}')); + const action = body.action; + if (String(url).includes('/quote/v2')) + return Response.json({ + steps: [ + { + id: 'deposit', + kind: 'transaction', + requestId: 'deposit-1', + depositAddress: '0x' + 'b'.repeat(40), + items: [{ status: 'incomplete', data: {} }], + }, + ], + fees: {}, + details: { + currencyOut: { amount: '1000000000', currency: { decimals: 8 } }, + expiresAt: Date.now() + 60000, + }, + }); + if (String(url).includes('/intents/status')) + return Response.json({ status: 'success' }); + if (action?.type === 'approveAgent') { + if (action.agentAddress === '0x' + '0'.repeat(40)) + environment.agents = []; + else + environment.agents.push({ + address: action.agentAddress, + validUntil: Number( + action.agentName.match(/valid_until (\d+)/)?.[1], + ), + }); + return Response.json({ status: 'ok', response: { type: 'default' } }); + } + if (action?.type === 'approveBuilderFee') { + expect(action).toMatchObject({ + builder: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + maxFeeRate: '0.01%', + }); + environment.builderFee = 10; + return Response.json({ status: 'ok', response: { type: 'default' } }); + } + if (body.type === 'maxBuilderFee') + return Response.json(environment.builderFee); + if (action) { + if (action.type === 'order') + expect(action.builder).toEqual({ + b: '0x71b09a08257078a4d642f7dd7315e656c837329a', + f: 10, + }); + const statuses = + action.type === 'order' ? [{ resting: { oid: 1 } }] : ['success']; + if (action.type === 'order') + environment.orders = [ + { coin: 'BTC', oid: 1, side: 'B', sz: '0.001', limitPx: '20000' }, + ]; + if (action.type === 'cancel') environment.orders = []; + return Response.json({ + status: 'ok', + response: { + type: action.type === 'batchModify' ? 'order' : action.type, + data: { statuses }, + }, + }); + } + if (body.type === 'meta') + return Response.json({ + universe: [{ name: 'BTC', szDecimals: 5, maxLeverage: 40 }], + marginTables: [], + }); + if (body.type === 'clearinghouseState') + return Response.json({ + marginSummary: { + accountValue: environment.balance, + totalMarginUsed: '0', + totalNtlPos: '0', + }, + withdrawable: environment.balance, + assetPositions: [], + }); + if (body.type === 'extraAgents') return Response.json(environment.agents); + if (body.type === 'frontendOpenOrders') + return Response.json(environment.orders); + if (body.type === 'allMids') return Response.json({ BTC: '20000' }); + throw new Error('Unexpected mocked request: ' + JSON.stringify(body)); + }), + ); +}); +afterEach(async () => { + await activeRunner?.shutdown(); + setActiveRunner(undefined); + delete process.env.BLOXWAP_E2E_AUTH; + vi.unstubAllGlobals(); + await rm(environment.home, { recursive: true, force: true }); +}); +async function command(name: string, input: Record = {}) { + const result = await dispatch([ + name, + '--config', + path, + '--json', + JSON.stringify(input), + ]); + expect(result.body, JSON.stringify(result.body)).toMatchObject({ ok: true }); + return (result.body as { data: Record }).data; +} +test('mocked full account lifecycle through real CLI router, SDK and runner RPC', async () => { + const initialized = await command('init', { qr: false }); + expect(initialized.requiresSignature).toBe(true); + expect(initialized.approvals).toMatchObject({ + builder: { typedData: { message: { maxFeeRate: '0.01%' } } }, + }); + expect( + ( + await command('init', { + qr: false, + signature: '0x' + '11'.repeat(64) + '1b', + }) + ).approved, + ).toBe(false); + expect( + ( + await command('init', { + qr: false, + builderSignature: '0x' + '11'.repeat(64) + '1b', + }) + ).approved, + ).toBe(true); + const deposit = await command('fund', { qr: false, amount: '10' }); + expect(deposit.depositId).toBe('deposit-1'); + environment.balance = '10'; + const status = await command('status'); + expect(status.deposits).toEqual([{ id: 'deposit-1', state: 'settled' }]); + expect(await command('market', { coin: 'BTC' })).toMatchObject({ + mid: '20000', + szDecimals: '5', + asset: '0', + }); + await command('run'); + expect(await rpcCall('balance', {}, socketPath())).toMatchObject({ + accountValue: '10', + }); + const placed = await command('order', { + coin: 'BTC', + side: 'buy', + size: '0.001', + price: '20000', + intentId: 'lifecycle', + }); + expect(placed).toMatchObject({ + builderFeeRate: '0.01%', + builderAddress: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + }); + await command('modify', { oid: 1, price: '20100' }); + await command('cancel', { all: true }); + const rotation = await command('rotate'); + expect(rotation.requiresSignature).toBe(true); + expect( + (await command('rotate', { signature: '0x' + '11'.repeat(64) + '1b' })) + .swapped, + ).toBe(true); + await command('revoke', { cancelOrders: true }); + const rejected = await dispatch([ + 'order', + '--config', + path, + '--json', + '{"coin":"BTC","side":"buy","size":"0.001","price":"20000"}', + ]); + expect(rejected.body).toMatchObject({ + ok: false, + code: 'AGENT_NOT_APPROVED', + }); + const { events } = await import('../src/runner/events.js'); + await events.append('fill', { tid: 7, oid: 1, fee: '0.01' }); + const entries = await readAudit( + join(environment.home, '.bloxwap', 'audit.jsonl'), + ); + expect( + entries.some( + (entry) => entry.intentId === 'lifecycle' && entry.stage === 'signature', + ), + ).toBe(true); + expect( + entries.some( + (entry) => entry.intentId === 'lifecycle' && entry.stage === 'fill', + ), + ).toBe(true); + expect(verifyAudit(entries)).toBe(true); + expect(await command('events')).toBeDefined(); +}, 20000); diff --git a/test/logger.test.ts b/test/logger.test.ts new file mode 100644 index 0000000..19f778a --- /dev/null +++ b/test/logger.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Logger } from '../src/core/logger.js'; +test('nested secrets are redacted and rotated lines retain correlation', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-logs-')); + const lines: string[] = []; + try { + const logger = new Logger({ + directory: dir, + maxBytes: 100, + requestId: 'request', + stderr: (line) => lines.push(line), + }); + logger.log('info', 'safe', { + nested: { authorizationKey: 'secret' }, + raw: '0x' + 'ab'.repeat(32), + token: 'sensitive', + }); + logger.log('warn', 'next'); + await logger.flush(); + const output = lines.join(''); + expect(output).not.toContain('secret'); + expect(output).not.toContain('sensitive'); + expect(output).not.toContain('ab'.repeat(32)); + expect( + lines.every((line) => JSON.parse(line).requestId === 'request'), + ).toBe(true); + const files = await readdir(dir); + expect(files.length).toBe(2); + expect(await readFile(join(dir, files[0]), 'utf8')).toContain('request'); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); +test('short opaque API and authorization keys are redacted by field name', async () => { + const { redact } = await import('../src/core/logger.js'); + expect( + redact({ apiKey: 'opaque', api_key: 'opaque', authKey: 'opaque' }), + ).toEqual({ + apiKey: '[REDACTED]', + api_key: '[REDACTED]', + authKey: '[REDACTED]', + }); +}); diff --git a/test/logs.test.ts b/test/logs.test.ts new file mode 100644 index 0000000..8a911a9 --- /dev/null +++ b/test/logs.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { readLogs } from '../src/commands/logs.js'; +test('logs filter level, date and request while preserving redaction', async () => { + const directory = await mkdtemp(join(tmpdir(), 'bloxwap-logread-')); + try { + await writeFile( + join(directory, 'runner.jsonl'), + [ + { level: 'info', time: '2026-01-01', requestId: 'a' }, + { + level: 'warn', + time: '2026-01-02', + requestId: 'b', + token: 'sensitive', + }, + ] + .map((value) => JSON.stringify(value)) + .join('\n') + '\n', + ); + expect( + await readLogs({ + directory, + level: 'warn', + since: '2026-01-02', + requestId: 'b', + }), + ).toEqual([ + { + level: 'warn', + time: '2026-01-02', + requestId: 'b', + token: '[REDACTED]', + }, + ]); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/test/mappers.test.ts b/test/mappers.test.ts new file mode 100644 index 0000000..ebec8a0 --- /dev/null +++ b/test/mappers.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from 'vitest'; +import { + mapHyperliquidError, + mapPrivyError, + mapRelayError, + mapNetworkError, +} from '../src/errors/mappers.js'; +test.each([ + ['Insufficient margin', 'INSUFFICIENT_MARGIN', false], + ['Order must have minimum value of $10', 'INVALID_SIZE', false], + ['Invalid price tick', 'INVALID_PRICE', false], + ['HTTP 429', 'RATE_LIMIT', true], + ['Nonce already used', 'NONCE_COLLISION', true], +])('maps fixture %s', (message, code, retryable) => { + expect(mapHyperliquidError(message).toJSON()).toMatchObject({ + code, + retryable, + }); +}); +test('policy, relay unknown and network flags redact details', () => { + expect(mapPrivyError('Policy denied').code).toBe('POLICY_REJECTED'); + expect( + mapRelayError({ message: 'unexpected', token: 'secret' }).toJSON(), + ).toMatchObject({ + code: 'UPSTREAM_ERROR', + details: { upstream: { token: '[REDACTED]' } }, + }); + expect(mapNetworkError(new Error('fetch failed')).retryable).toBe(true); +}); diff --git a/test/modify.test.ts b/test/modify.test.ts new file mode 100644 index 0000000..22b0937 --- /dev/null +++ b/test/modify.test.ts @@ -0,0 +1,114 @@ +import { expect, test } from 'vitest'; +import { modifyOrders, modifySchema } from '../src/commands/modify.js'; +import { mockTrading } from './fixtures.js'; +import { migrate } from '../src/core/config.js'; +test('modify rounds replacements and sends batch only after validation', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { coin: 'BTC', oid: 1, sz: '0.001', limitPx: '20000', side: 'B' }, + ]; + await modifyOrders( + config, + modifySchema.parse({ + modifications: [{ oid: 1, price: '20000.5', size: '0.0012345' }], + }), + services, + ); + expect(services.exchange.batchModify).toHaveBeenCalledWith({ + modifies: [ + { + oid: 1, + order: { + a: 0, + b: true, + p: '20000', + s: '0.00123', + r: false, + t: { limit: { tif: 'Gtc' } }, + }, + }, + ], + }); + await expect( + modifyOrders(config, modifySchema.parse({ oid: 4 }), services), + ).rejects.toMatchObject({ code: 'ORDER_NOT_FOUND' }); +}); +test('modify preserves reduce-only and tif', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { + coin: 'BTC', + oid: 1, + sz: '0.001', + limitPx: '20000', + side: 'A', + reduceOnly: true, + tif: 'Alo', + }, + ]; + await modifyOrders( + config, + modifySchema.parse({ oid: 1, price: '20001' }), + services, + ); + expect(services.exchange.batchModify).toHaveBeenCalledWith( + expect.objectContaining({ + modifies: [ + expect.objectContaining({ + order: expect.objectContaining({ + r: true, + t: { limit: { tif: 'Alo' } }, + }), + }), + ], + }), + ); +}); +test('trigger modifications retain TP/SL semantics', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { + coin: 'BTC', + oid: 1, + sz: '0.001', + limitPx: '20000', + side: 'A', + reduceOnly: true, + isTrigger: true, + triggerPx: '19900', + orderType: 'Stop Market', + }, + ]; + await modifyOrders( + config, + modifySchema.parse({ oid: 1, size: '0.0005' }), + services, + ); + expect(services.exchange.batchModify).toHaveBeenCalledWith( + expect.objectContaining({ + modifies: [ + expect.objectContaining({ + order: expect.objectContaining({ + r: true, + t: { trigger: { triggerPx: '19900', isMarket: true, tpsl: 'sl' } }, + }), + }), + ], + }), + ); +}); diff --git a/test/order.test.ts b/test/order.test.ts new file mode 100644 index 0000000..401e473 --- /dev/null +++ b/test/order.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from 'vitest'; +import { prepareOrder, orderSchema } from '../src/commands/order.js'; +import { migrate } from '../src/core/config.js'; +import { mockTrading } from './fixtures.js'; + +test('order rounds limit and bounds market IOC', async () => { + const clients = mockTrading(); + expect( + await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'buy', + size: '0.0012345', + price: '20000.1', + }), + clients, + ), + ).toMatchObject({ + a: 0, + s: '0.00123', + p: '20000', + t: { limit: { tif: 'Gtc' } }, + }); + expect( + await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'sell', + size: '0.001', + type: 'market', + }), + clients, + ), + ).toMatchObject({ p: '19800', t: { limit: { tif: 'Ioc' } } }); + await expect( + prepareOrder( + migrate({}), + orderSchema.parse({ coin: 'BTC', side: 'buy', size: '0', price: '1' }), + clients, + ), + ).rejects.toMatchObject({ code: 'INVALID_SIZE' }); +}); +test('sell IOC rounding stays inside the slippage floor', async () => { + const clients = mockTrading(); + clients.mids = async () => ({ BTC: '123.456' }); + const order = await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'sell', + size: '0.001', + type: 'market', + slippage: 0.01, + }), + clients, + ); + expect(Number(order.p)).toBeGreaterThanOrEqual(123.456 * 0.99); + const trigger = await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'sell', + size: '0.001', + type: 'sl', + triggerPrice: '100', + reduceOnly: true, + }), + clients, + ); + expect(trigger.t).toMatchObject({ trigger: { isMarket: true, tpsl: 'sl' } }); +}); diff --git a/test/policy.test.ts b/test/policy.test.ts new file mode 100644 index 0000000..ff3e17a --- /dev/null +++ b/test/policy.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest'; +import { assertTradeAction, policyFor } from '../src/wallet/policy.js'; +test('trade-only action whitelist rejects withdrawal and approval', () => { + for (const action of [ + 'withdraw', + 'withdraw3', + 'usdSend', + 'spotSend', + 'approveAgent', + 'approveBuilderFee', + 'usdClassTransfer', + ]) + expect(() => assertTradeAction(action)).toThrow( + expect.objectContaining({ code: 'POLICY_REJECTED' }), + ); + assertTradeAction('order'); + expect(policyFor('mainnet').rules[0].conditions[2].value).toBe('a'); +}); diff --git a/test/privy.integration.test.ts b/test/privy.integration.test.ts new file mode 100644 index 0000000..8567f3f --- /dev/null +++ b/test/privy.integration.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest'; +import { privyClient } from '../src/wallet/privy.js'; +import { migrate } from '../src/core/config.js'; +test.skipIf(!process.env.BLOXWAP_PRIVY_SANDBOX)( + 'Privy sandbox credentials', + async () => { + const client = await privyClient( + migrate({ privyAppId: process.env.BLOXWAP_PRIVY_APP_ID }), + ); + expect(await client.wallets().list()).toBeDefined(); + }, +); diff --git a/test/privy.test.ts b/test/privy.test.ts new file mode 100644 index 0000000..6e02b3d --- /dev/null +++ b/test/privy.test.ts @@ -0,0 +1,45 @@ +import { expect, test, vi } from 'vitest'; +import type { PrivyClient } from '@privy-io/node'; +import { privySigner, resolveSecret } from '../src/wallet/privy.js'; +import { migrate } from '../src/core/config.js'; +test('remote typed-data signing preserves EIP-712 and maps denial', async () => { + const signTypedData = vi.fn().mockResolvedValue({ signature: '0x1234' }); + const client = { + wallets: () => ({ ethereum: () => ({ signTypedData }) }), + } as unknown as PrivyClient; + process.env.BLOXWAP_TEST_AUTH = 'test-key'; + try { + const signer = await privySigner( + migrate({ + agentAddress: '0x' + 'a'.repeat(40), + privyWalletId: 'wallet', + authorizationKeyRef: 'env:BLOXWAP_TEST_AUTH', + }), + client, + ); + const data = { + domain: { + name: 'Exchange', + version: '1', + chainId: 1337, + verifyingContract: ('0x' + '0'.repeat(40)) as `0x${string}`, + }, + types: { Agent: [{ name: 'source', type: 'string' }] }, + primaryType: 'Agent', + message: { source: 'b' }, + }; + expect(await signer.signTypedData(data)).toBe('0x1234'); + expect(signTypedData.mock.calls[0][1].params.typed_data.primary_type).toBe( + 'Agent', + ); + signTypedData.mockRejectedValue(new Error('Policy denied')); + await expect(signer.signTypedData(data)).rejects.toMatchObject({ + code: 'POLICY_REJECTED', + }); + } finally { + delete process.env.BLOXWAP_TEST_AUTH; + } + expect( + await resolveSecret('env:BLOXWAP_SECRET', { BLOXWAP_SECRET: 'ok' }), + ).toBe('ok'); +}); diff --git a/test/qr.test.ts b/test/qr.test.ts new file mode 100644 index 0000000..feb7a4f --- /dev/null +++ b/test/qr.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest'; +import { renderQr } from '../src/funding/qr.js'; +test('QR deterministic terminal snapshot stays off stdout', async () => { + let stderr = ''; + const result = await renderQr('bloxwap:test', { + stderr: (value) => { + stderr = value; + }, + }); + expect(result.qrAscii).toMatchSnapshot(); + expect(stderr).toContain(result.qrAscii); + expect(await renderQr('disabled', { enabled: false })).toEqual({ + qrAscii: null, + qrPath: null, + }); +}); diff --git a/test/read.test.ts b/test/read.test.ts new file mode 100644 index 0000000..b97f4d4 --- /dev/null +++ b/test/read.test.ts @@ -0,0 +1,20 @@ +import { accountFixture } from './fixtures.js'; +import { expect, test } from 'vitest'; +import { + accountBalance, + positions, + expiryStatus, +} from '../src/commands/read.js'; +import { migrate } from '../src/core/config.js'; + +test('read fixtures retain numeric strings and exact balances', () => { + expect(accountBalance(accountFixture).accountValueUnits).toBe('10012345678'); + expect(positions(accountFixture)[0]).toMatchObject({ + size: '0.001', + leverage: '5', + entryPrice: '20000', + }); + expect( + expiryStatus(migrate({ agentExpiry: 86400000 }), 0).warnings, + ).toHaveLength(3); +}); diff --git a/test/relay.test.ts b/test/relay.test.ts new file mode 100644 index 0000000..fc446d2 --- /dev/null +++ b/test/relay.test.ts @@ -0,0 +1,30 @@ +import { quoteFixture } from './fixtures.js'; +import { expect, test, vi } from 'vitest'; +import { RelayClient, PERPS_USDC, parseQuote } from '../src/funding/relay.js'; + +test('Relay request uses perps destination and parses fees/address', async () => { + const fetcher = vi.fn( + async () => new Response(JSON.stringify(quoteFixture)), + ) as unknown as typeof fetch; + const client = new RelayClient('https://api.relay.link', fetcher); + const quote = await client.quote({ + user: 'master', + recipient: 'master', + originChainId: 42161, + originCurrency: 'USDC', + amount: '1000000', + destinationChainId: 1337, + destinationCurrency: PERPS_USDC, + useDepositAddress: true, + tradeType: 'EXACT_INPUT', + }); + expect(parseQuote(quote)).toMatchObject({ + depositId: 'deposit-1', + expectedReceived: '100000000', + fees: { + app: { amount: '10' }, + gas: { amount: '20' }, + relayer: { amount: '30' }, + }, + }); +}); diff --git a/test/retry.test.ts b/test/retry.test.ts new file mode 100644 index 0000000..f73c5e5 --- /dev/null +++ b/test/retry.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { retry } from '../src/core/retry.js'; +import { nextNonce } from '../src/core/nonce.js'; +import { AgentCliError } from '../src/errors/index.js'; +test('bounded backoff only retries temporary errors', async () => { + let attempts = 0; + const sleeps: number[] = []; + expect( + await retry( + async () => { + if (++attempts < 3) throw new AgentCliError('RATE_LIMIT', 'wait'); + return 7; + }, + { + baseMs: 100, + random: () => 0.5, + sleep: async (ms) => { + sleeps.push(ms); + }, + }, + ), + ).toBe(7); + expect(sleeps).toEqual([100, 200]); + await expect( + retry(async () => { + throw new AgentCliError('POLICY_REJECTED', 'stop'); + }), + ).rejects.toMatchObject({ retryable: false }); +}); +test('persisted nonces serialize concurrent callers and restarts', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-nonce-')); + const signer = '0x' + 'a'.repeat(40); + try { + const values = await Promise.all( + Array.from({ length: 8 }, () => nextNonce(signer, dir, () => 1000)), + ); + expect(new Set(values).size).toBe(8); + expect(await nextNonce(signer, dir, () => 500)).toBe(1008); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/revoke.test.ts b/test/revoke.test.ts new file mode 100644 index 0000000..d407b81 --- /dev/null +++ b/test/revoke.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest'; +import { markRevoked } from '../src/commands/revoke.js'; +import { assertCanTrade } from '../src/core/trading.js'; +import { migrate } from '../src/core/config.js'; +test('revocation immediately blocks every trading entrypoint', async () => { + const config = migrate({ + approved: true, + agentExpiry: Date.now() + 10000, + masterAddress: '0x' + 'a'.repeat(40), + }); + markRevoked(config); + await expect(assertCanTrade(config)).rejects.toMatchObject({ + code: 'AGENT_NOT_APPROVED', + }); +}); diff --git a/test/risk.test.ts b/test/risk.test.ts new file mode 100644 index 0000000..f89e44d --- /dev/null +++ b/test/risk.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from 'vitest'; +import { evaluateRisk } from '../src/core/risk.js'; +import { migrate } from '../src/core/config.js'; +const intent = { + coin: 'BTC', + side: 'buy' as const, + size: '2', + price: '10', + leverage: 2, +}; +const state = { + positionSize: '1', + dailyLoss: '5', + openOrders: 2, + marketMaxLeverage: 40, +}; +test.each([ + { allowedMarkets: ['ETH'] }, + { limits: { maxOrderSize: 1 } }, + { limits: { maxNotional: 19 } }, + { limits: { maxPositionNotional: 29 } }, + { limits: { dailyLoss: 4 } }, + { limits: { maxOpenOrders: 2 } }, + { limits: { maxLeverage: 1 } }, +])('enforces each risk bound %j', (config) => { + expect(() => evaluateRisk(migrate(config), intent, state)).toThrow( + expect.objectContaining({ code: 'POLICY_REJECTED' }), + ); +}); +test('reduce-only cannot increase exposure and decimals remain exact', () => { + expect(evaluateRisk(migrate({}), intent, state).allowed).toBe(true); + expect(() => + evaluateRisk(migrate({}), { ...intent, reduceOnly: true }, state), + ).toThrow(); + expect( + evaluateRisk( + migrate({}), + { ...intent, side: 'sell', reduceOnly: true }, + state, + ).allowed, + ).toBe(true); +}); +test('opposing pending orders cannot offset worst-case exposure', () => { + expect(() => + evaluateRisk(migrate({ limits: { maxPositionNotional: 50 } }), intent, { + ...state, + pendingBuySize: '10', + pendingSellSize: '10', + }), + ).toThrow(expect.objectContaining({ code: 'POLICY_REJECTED' })); +}); diff --git a/test/rotate.test.ts b/test/rotate.test.ts new file mode 100644 index 0000000..d3bb6c7 --- /dev/null +++ b/test/rotate.test.ts @@ -0,0 +1,24 @@ +import { expect, test, vi } from 'vitest'; +import { rotationHandoff, type Rotation } from '../src/commands/rotate.js'; +test('rotation never exposes old revocation until approval verification and swap', async () => { + const rotation = { + next: {}, + swapped: false, + revoke: { action: { type: 'approveAgent' } }, + } as unknown as Rotation; + const swap = vi.fn(async () => {}), + persist = vi.fn(async () => {}); + await expect( + rotationHandoff(rotation, { verify: async () => false, swap, persist }), + ).rejects.toMatchObject({ code: 'AGENT_NOT_APPROVED' }); + expect(swap).not.toHaveBeenCalled(); + expect( + await rotationHandoff(rotation, { + verify: async () => true, + swap, + persist, + }), + ).toBe(rotation.revoke); + expect(rotation.swapped).toBe(true); + expect(persist).toHaveBeenCalledOnce(); +}); diff --git a/test/router.test.ts b/test/router.test.ts new file mode 100644 index 0000000..4ac0653 --- /dev/null +++ b/test/router.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { dispatch, register } from '../src/core/router.js'; +register('echo', { + description: 'test', + schema: z.object({ value: z.string() }), + execute: async (input) => input, +}); +register('crash', { + description: 'test', + schema: z.object({}), + execute: async () => { + throw new Error('unexpected'); + }, +}); +test('flags and stdin give one success envelope', async () => { + expect((await dispatch(['echo', '--value', 'hello'])).body).toEqual({ + ok: true, + data: { value: 'hello' }, + }); + expect((await dispatch(['echo'], '{"value":"stdin"}')).exitCode).toBe(0); + expect( + (await dispatch(['echo', '--json', '{"value":"json"}'])).exitCode, + ).toBe(0); +}); +test('validation and unexpected failures normalize', async () => { + expect((await dispatch(['echo'])).body).toMatchObject({ + ok: false, + code: 'CONFIG_INVALID', + }); + expect((await dispatch(['crash'])).body).toMatchObject({ + ok: false, + code: 'INTERNAL', + }); + expect((await dispatch(['schema', 'echo'])).exitCode).toBe(0); +}); +test('output schemas expose data fields and the fixed error catalog', async () => { + const { schemas } = await import('../src/core/router.js'); + register('balance', { + description: 'balance', + schema: z.object({}), + execute: async () => ({}), + }); + const schema = schemas('balance'); + expect(schema.output.oneOf[0].properties.data).toMatchObject({ + required: [ + 'accountValue', + 'accountValueUnits', + 'withdrawable', + 'marginUsed', + ], + }); +}); +test('unknown fields are rejected instead of silently changing trade intent', async () => { + expect( + (await dispatch(['echo', '--json', '{"value":"hello","typo":true}'])).body, + ).toMatchObject({ ok: false, code: 'CONFIG_INVALID' }); +}); +test('JSON prototype fields cannot conceal master secrets during parsing', async () => { + const payload = '{"__proto__":{"privateKey":"' + 'ab'.repeat(32) + '"}}'; + expect((await dispatch(['echo', '--json', payload])).body).toMatchObject({ + ok: false, + code: 'POLICY_REJECTED', + }); +}); diff --git a/test/rpc.test.ts b/test/rpc.test.ts new file mode 100644 index 0000000..92bc9cc --- /dev/null +++ b/test/rpc.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Runner } from '../src/runner/lifecycle.js'; +import { rpcServer, rpcCall, socketPath } from '../src/runner/rpc.js'; +import { migrate } from '../src/core/config.js'; +test('private RPC framing, multi-client serialization and normalized errors', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-rpc-')); + const runner = new Runner(migrate({}), dir); + try { + await rpcServer(runner, { pretty: false }, async (name, input) => ({ + name, + input, + })); + const path = socketPath(dir); + if (process.platform !== 'win32') + expect((await stat(path)).mode & 0o777).toBe(0o600); + expect( + await Promise.all([ + rpcCall('status.get', {}, path), + rpcCall('balance', {}, path), + ]), + ).toEqual([ + { name: 'status', input: {} }, + { name: 'balance', input: {} }, + ]); + await expect(rpcCall('withdraw', {}, path)).rejects.toMatchObject({ + code: 'CONFIG_INVALID', + }); + } finally { + await runner.shutdown(); + await rm(dir, { recursive: true, force: true }); + } +}); +test('RPC refuses a client targeting another network', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-rpc-network-')); + const runner = new Runner(migrate({ network: 'testnet' }), dir); + try { + await rpcServer(runner, { pretty: false }, async () => ({ ok: true })); + await expect( + rpcCall('order', { __context: { network: 'mainnet' } }, socketPath(dir)), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + } finally { + await runner.shutdown(); + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/runner.test.ts b/test/runner.test.ts new file mode 100644 index 0000000..3cb44ba --- /dev/null +++ b/test/runner.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { Runner, runningInfo } from '../src/runner/lifecycle.js'; +import { migrate } from '../src/core/config.js'; +test('single instance and graceful cleanup preserve external state', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-runner-')); + const signer = { + address: ('0x' + 'a'.repeat(40)) as `0x${string}`, + signTypedData: async () => '0x123' as `0x${string}`, + }; + const first = new Runner(migrate({}), dir); + const second = new Runner(migrate({}), dir); + try { + expect((await first.start(async () => signer)).existing).toBe(false); + expect((await second.start(async () => signer)).existing).toBe(true); + expect(await runningInfo(join(dir, 'runner.pid'))).toMatchObject({ + pid: process.pid, + }); + await first.shutdown(); + expect(await runningInfo(join(dir, 'runner.pid'))).toBeNull(); + expect(await readFile(join(dir, 'logs', 'runner.jsonl'), 'utf8')).toContain( + 'Runner stopped', + ); + } finally { + await first.shutdown(); + await rm(dir, { recursive: true, force: true }); + } +}); +test('a dead PID restarts and readiness is explicit', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-runner-restart-')); + const runner = new Runner(migrate({}), dir); + try { + const { writeFile } = await import('node:fs/promises'); + await writeFile( + join(dir, 'runner.pid'), + JSON.stringify({ + pid: 2147483647, + network: 'testnet', + startedAt: 0, + ready: true, + }), + ); + await runner.start(async () => ({ + address: ('0x' + 'a'.repeat(40)) as `0x${string}`, + signTypedData: async () => '0x1', + })); + expect((await runningInfo(join(dir, 'runner.pid')))?.ready).toBe(false); + await runner.markReady(); + expect((await runningInfo(join(dir, 'runner.pid')))?.ready).toBe(true); + } finally { + await runner.shutdown(); + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/safety.test.ts b/test/safety.test.ts new file mode 100644 index 0000000..0d63239 --- /dev/null +++ b/test/safety.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from 'vitest'; +import { assertSafeInput } from '../src/wallet/safety.js'; +import { dispatch } from '../src/core/router.js'; +import { redact } from '../src/core/logger.js'; +test('secret detector covers keys, seeds and nested ignored fields', () => { + for (const value of [ + 'ab'.repeat(32), + '0x' + '12'.repeat(32), + 'abandon '.repeat(11) + 'about', + { ignored: { seedPhrase: 'secret' } }, + ['abandon '.repeat(12).trim()], + ]) + expect(() => assertSafeInput(value)).toThrow(); + assertSafeInput({ + signature: '0x' + '12'.repeat(65), + address: '0x' + 'a'.repeat(40), + }); +}); +test('environment secrets are scrubbed from arbitrary messages', async () => { + process.env.BLOXWAP_SECRET_TOKEN = 'sensitive-value'; + try { + expect(redact('failure: sensitive-value')).toBe('failure: [REDACTED]'); + expect( + ( + await dispatch([ + 'config', + '--json', + JSON.stringify({ privateKey: 'ab'.repeat(32) }), + ]) + ).body, + ).toMatchObject({ ok: false }); + } finally { + delete process.env.BLOXWAP_SECRET_TOKEN; + } +}); +test('secrets embedded in free-form intent text are also refused', () => { + expect(() => + assertSafeInput('key: 0x' + '12'.repeat(32) + ' please use it'), + ).toThrow(); + expect(() => + assertSafeInput('recovery words: ' + 'abandon '.repeat(11) + 'about'), + ).toThrow(); + assertSafeInput('signature: 0x' + '12'.repeat(65)); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..7907761 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,20 @@ +import { afterAll, beforeAll, vi } from 'vitest'; +import { mkdir, rm } from 'node:fs/promises'; +const testHome = vi.hoisted( + () => + `/tmp/bloxwap-unit-${process.pid}-${Math.random().toString(16).slice(2)}`, +); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + homedir: () => testHome, +})); +beforeAll(async () => { + await mkdir(testHome, { recursive: true, mode: 0o700 }); +}); +afterAll(async () => { + await rm(testHome, { recursive: true, force: true }); +}); +// Explicit service fixtures may replace this stub; accidental unit-test network fails. +vi.stubGlobal('fetch', async () => { + throw new Error('Unit test attempted an unmocked network request'); +}); diff --git a/test/skill.test.ts b/test/skill.test.ts new file mode 100644 index 0000000..ba3e662 --- /dev/null +++ b/test/skill.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { installSkill } from '../src/commands/skill.js'; +import { skillContent, cliVersion } from '../src/core/skill-content.js'; +import packageInfo from '../package.json' with { type: 'json' }; +test('installs correct agent paths with version match and idempotency', async () => { + const home = await mkdtemp(join(tmpdir(), 'bloxwap-skill-')); + try { + expect(cliVersion).toBe(packageInfo.version); + const codex = await installSkill('codex', { + home, + codexHome: join(home, 'custom-codex'), + }); + expect(codex.path).toBe( + join(home, 'custom-codex', 'skills', 'agent-cli', 'SKILL.md'), + ); + expect(await readFile(codex.path, 'utf8')).toBe(skillContent); + expect( + ( + await installSkill('codex', { + home, + codexHome: join(home, 'custom-codex'), + }) + ).changed, + ).toBe(false); + const claude = await installSkill('claude', { home }); + expect(claude.path).toBe( + join(home, '.claude', 'skills', 'agent-cli', 'SKILL.md'), + ); + await writeFile(claude.path, 'custom'); + await expect(installSkill('claude', { home })).rejects.toMatchObject({ + code: 'CONFIG_INVALID', + }); + expect((await installSkill('claude', { home, force: true })).changed).toBe( + true, + ); + } finally { + await rm(home, { recursive: true, force: true }); + } +}); diff --git a/test/smoke.test.ts b/test/smoke.test.ts new file mode 100644 index 0000000..abad2b5 --- /dev/null +++ b/test/smoke.test.ts @@ -0,0 +1,2 @@ +import { expect, test } from 'vitest'; +test('strict ESM runtime', () => expect(import.meta.url).toMatch(/^file:/)); diff --git a/test/subscriptions.test.ts b/test/subscriptions.test.ts new file mode 100644 index 0000000..185e2aa --- /dev/null +++ b/test/subscriptions.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Runner } from '../src/runner/lifecycle.js'; +import { Subscriptions, subscriptionSet } from '../src/runner/subscriptions.js'; +import { migrate } from '../src/core/config.js'; +test('reconnect gap-fill dedupes fills and persists subscription cursor', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-ws-')); + const runner = new Runner(migrate({ allowedMarkets: ['BTC'] }), dir); + const events: unknown[] = []; + let state: (value: string) => void = () => {}; + const backend = { + subscribe: async () => ({ unsubscribe: async () => {} }), + onState: (listener: typeof state) => { + state = listener; + }, + close: async () => {}, + gap: async () => [ + { tid: 1, time: 1 }, + { tid: 2, time: 2 }, + ], + }; + const manager = new Subscriptions(runner, backend, async (type, data) => { + events.push({ type, data }); + }); + try { + await manager.start(); + await manager.fill({ tid: 1, time: 1 }); + state('connected'); + state('reconnecting'); + state('connected'); + await manager.flush(); + expect( + events.filter((e) => (e as { type: string }).type === 'fill'), + ).toHaveLength(2); + expect(subscriptionSet(runner.config)).toContain('l2Book:BTC'); + await runner.shutdown(); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..996ddb8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": ["src", "test"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..17ad725 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + test: { + setupFiles: ['test/setup.ts'], + exclude: ['test/*.integration.test.ts', 'node_modules/**'], + }, +}); diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..44ca262 --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + test: { include: ['test/*.integration.test.ts'] }, +});