From c10ee828f39ad98f0c8ad6dea1fcee4c02e83f6a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 07:19:00 +0000 Subject: [PATCH] Add octobot_edge package: universal CCXT wrapper with Rust core Move edge-ccxt into OctoBot/packages/edge as a new Pants-managed package. The package provides a shared Rust core for crypto primitives (HMAC, hashing, buffer ops) with bindings for Python (PyO3/maturin), Node.js/Bun (NAPI), browser/Deno (WASM), and React Native (UniFFI). Structure: - rust/core: Pure Rust crypto library (HMAC-SHA256/512, SHA-256/512, MD5, etc.) - rust/wasm: wasm-bindgen target for browser + Deno - rust/napi: napi-rs target for Node.js + Bun - rust/py: PyO3/maturin target for Python - octobot_edge/: Python package with ccxt exchange factory and HMAC patching - js/: npm package (@octobot/edge) with multi-target TypeScript builds - CI: Dedicated edge.yml workflow + integration into main test matrix https://claude.ai/code/session_01A41Pp6ZEett2vEfjJCkkXx --- .github/workflows/edge.yml | 335 + .github/workflows/main.yml | 1 + .gitignore | 15 + BUILD | 2 + packages/edge/BUILD | 45 + packages/edge/README.md | 281 + packages/edge/full_requirements.txt | 3 + packages/edge/js/biome.json | 17 + packages/edge/js/esbuild.mjs | 58 + packages/edge/js/jest.config.js | 12 + packages/edge/js/package-lock.json | 7812 +++++++++++++++++ packages/edge/js/package.json | 83 + .../edge/js/scripts/postprocess-uniffi.js | 35 + .../edge/js/scripts/sync-wasm-artifacts.js | 26 + packages/edge/js/src/exchange/index.ts | 1 + packages/edge/js/src/exchange/patch.ts | 47 + packages/edge/js/src/index.ts | 87 + packages/edge/js/src/polyfills/browser.ts | 158 + packages/edge/js/src/polyfills/crypto.ts | 72 + packages/edge/js/src/polyfills/detect.ts | 12 + packages/edge/js/src/polyfills/encoding.ts | 98 + packages/edge/js/src/polyfills/mobile.ts | 296 + packages/edge/js/src/polyfills/node.ts | 46 + packages/edge/js/src/rust-bridge/native.ts | 34 + packages/edge/js/src/rust-bridge/types.ts | 20 + packages/edge/js/src/rust-bridge/wasm.ts | 48 + .../edge/js/tests/binance.integration.test.ts | 218 + packages/edge/js/tests/browser.test.ts | 131 + packages/edge/js/tests/crypto.test.ts | 125 + packages/edge/js/tests/e2e-advanced.test.ts | 646 ++ packages/edge/js/tests/e2e.test.ts | 311 + packages/edge/js/tests/encoding.test.ts | 133 + .../js/tests/exchange.integration.test.ts | 254 + packages/edge/js/tests/polyfills.node.test.ts | 49 + packages/edge/js/tests/runtime-env.test.ts | 321 + packages/edge/js/tsconfig.json | 19 + packages/edge/js/tsup.config.ts | 61 + packages/edge/js/ubrn.config.yaml | 10 + packages/edge/octobot_edge/__init__.py | 14 + packages/edge/octobot_edge/_native.pyi | 5 + packages/edge/octobot_edge/crypto/__init__.py | 3 + packages/edge/octobot_edge/crypto/hmac.py | 40 + .../edge/octobot_edge/exchange/__init__.py | 3 + packages/edge/octobot_edge/exchange/client.py | 55 + .../edge/octobot_edge/exchange/normalize.py | 44 + packages/edge/pyproject.toml | 21 + packages/edge/requirements.txt | 1 + packages/edge/rust/Cargo.lock | 2073 +++++ packages/edge/rust/Cargo.toml | 15 + packages/edge/rust/core/Cargo.toml | 24 + packages/edge/rust/core/src/lib.rs | 537 ++ packages/edge/rust/mobile/Cargo.toml | 17 + packages/edge/rust/mobile/build.rs | 3 + packages/edge/rust/mobile/src/lib.rs | 151 + packages/edge/rust/napi/Cargo.toml | 16 + packages/edge/rust/napi/build.rs | 5 + packages/edge/rust/napi/src/lib.rs | 81 + packages/edge/rust/py/Cargo.toml | 12 + packages/edge/rust/py/pyproject.toml | 8 + packages/edge/rust/py/src/lib.rs | 45 + packages/edge/rust/wasm/Cargo.toml | 16 + packages/edge/rust/wasm/src/lib.rs | 160 + packages/edge/tests/__init__.py | 0 packages/edge/tests/static/binance_order.json | 46 + .../edge/tests/static/binance_ticker.json | 41 + packages/edge/tests/static/hmac_vectors.json | 40 + packages/edge/tests/test_crypto.py | 104 + packages/edge/tests/test_e2e.py | 334 + packages/edge/tests/test_e2e_advanced.py | 654 ++ packages/edge/tests/test_exchange.py | 125 + packages/edge/tests/test_integration.py | 344 + pants.toml | 1 + 72 files changed, 16960 insertions(+) create mode 100644 .github/workflows/edge.yml create mode 100644 packages/edge/BUILD create mode 100644 packages/edge/README.md create mode 100644 packages/edge/full_requirements.txt create mode 100644 packages/edge/js/biome.json create mode 100644 packages/edge/js/esbuild.mjs create mode 100644 packages/edge/js/jest.config.js create mode 100644 packages/edge/js/package-lock.json create mode 100644 packages/edge/js/package.json create mode 100644 packages/edge/js/scripts/postprocess-uniffi.js create mode 100644 packages/edge/js/scripts/sync-wasm-artifacts.js create mode 100644 packages/edge/js/src/exchange/index.ts create mode 100644 packages/edge/js/src/exchange/patch.ts create mode 100644 packages/edge/js/src/index.ts create mode 100644 packages/edge/js/src/polyfills/browser.ts create mode 100644 packages/edge/js/src/polyfills/crypto.ts create mode 100644 packages/edge/js/src/polyfills/detect.ts create mode 100644 packages/edge/js/src/polyfills/encoding.ts create mode 100644 packages/edge/js/src/polyfills/mobile.ts create mode 100644 packages/edge/js/src/polyfills/node.ts create mode 100644 packages/edge/js/src/rust-bridge/native.ts create mode 100644 packages/edge/js/src/rust-bridge/types.ts create mode 100644 packages/edge/js/src/rust-bridge/wasm.ts create mode 100644 packages/edge/js/tests/binance.integration.test.ts create mode 100644 packages/edge/js/tests/browser.test.ts create mode 100644 packages/edge/js/tests/crypto.test.ts create mode 100644 packages/edge/js/tests/e2e-advanced.test.ts create mode 100644 packages/edge/js/tests/e2e.test.ts create mode 100644 packages/edge/js/tests/encoding.test.ts create mode 100644 packages/edge/js/tests/exchange.integration.test.ts create mode 100644 packages/edge/js/tests/polyfills.node.test.ts create mode 100644 packages/edge/js/tests/runtime-env.test.ts create mode 100644 packages/edge/js/tsconfig.json create mode 100644 packages/edge/js/tsup.config.ts create mode 100644 packages/edge/js/ubrn.config.yaml create mode 100644 packages/edge/octobot_edge/__init__.py create mode 100644 packages/edge/octobot_edge/_native.pyi create mode 100644 packages/edge/octobot_edge/crypto/__init__.py create mode 100644 packages/edge/octobot_edge/crypto/hmac.py create mode 100644 packages/edge/octobot_edge/exchange/__init__.py create mode 100644 packages/edge/octobot_edge/exchange/client.py create mode 100644 packages/edge/octobot_edge/exchange/normalize.py create mode 100644 packages/edge/pyproject.toml create mode 100644 packages/edge/requirements.txt create mode 100644 packages/edge/rust/Cargo.lock create mode 100644 packages/edge/rust/Cargo.toml create mode 100644 packages/edge/rust/core/Cargo.toml create mode 100644 packages/edge/rust/core/src/lib.rs create mode 100644 packages/edge/rust/mobile/Cargo.toml create mode 100644 packages/edge/rust/mobile/build.rs create mode 100644 packages/edge/rust/mobile/src/lib.rs create mode 100644 packages/edge/rust/napi/Cargo.toml create mode 100644 packages/edge/rust/napi/build.rs create mode 100644 packages/edge/rust/napi/src/lib.rs create mode 100644 packages/edge/rust/py/Cargo.toml create mode 100644 packages/edge/rust/py/pyproject.toml create mode 100644 packages/edge/rust/py/src/lib.rs create mode 100644 packages/edge/rust/wasm/Cargo.toml create mode 100644 packages/edge/rust/wasm/src/lib.rs create mode 100644 packages/edge/tests/__init__.py create mode 100644 packages/edge/tests/static/binance_order.json create mode 100644 packages/edge/tests/static/binance_ticker.json create mode 100644 packages/edge/tests/static/hmac_vectors.json create mode 100644 packages/edge/tests/test_crypto.py create mode 100644 packages/edge/tests/test_e2e.py create mode 100644 packages/edge/tests/test_e2e_advanced.py create mode 100644 packages/edge/tests/test_exchange.py create mode 100644 packages/edge/tests/test_integration.py diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml new file mode 100644 index 0000000000..d1486c0411 --- /dev/null +++ b/.github/workflows/edge.yml @@ -0,0 +1,335 @@ +name: OctoBot-Edge CI + +on: + push: + branches: + - 'master' + - 'dev' + tags: + - '*' + paths: + - 'packages/edge/**' + pull_request: + paths: + - 'packages/edge/**' + +permissions: read-all + +jobs: + rust-tests: + name: Rust tests — ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: macos-latest + target: aarch64-apple-darwin + - os: windows-latest + target: x86_64-pc-windows-msvc + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + packages/edge/rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('packages/edge/rust/**/Cargo.toml') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Run core Rust tests + working-directory: packages/edge/rust + run: cargo test --manifest-path core/Cargo.toml + + build-wasm: + name: Build WASM + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Install wasm-pack + run: cargo install wasm-pack --version 0.13.1 + + - name: Build WASM + working-directory: packages/edge/rust/wasm + run: wasm-pack build --target bundler --out-dir ../../js/src/rust-bridge/pkg + + - name: Upload WASM artifact + uses: actions/upload-artifact@v4 + with: + name: edge-wasm + path: packages/edge/js/src/rust-bridge/pkg/ + if-no-files-found: error + + build-napi: + name: Build NAPI — ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + artifact_suffix: linux-x64-gnu + - os: macos-latest + target: aarch64-apple-darwin + artifact_suffix: darwin-arm64 + - os: windows-latest + target: x86_64-pc-windows-msvc + artifact_suffix: win32-x64-msvc + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli@3.0.0 + + - name: Build NAPI addon + working-directory: packages/edge/rust/napi + run: | + napi build --platform --release \ + --js ../../js/src/rust-bridge/native.js \ + --dts ../../js/src/rust-bridge/native.d.ts + + - name: Upload NAPI artifact + uses: actions/upload-artifact@v4 + with: + name: edge-napi-${{ matrix.artifact_suffix }} + path: packages/edge/rust/napi/*.node + if-no-files-found: error + + build-python: + name: Build Python extension + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13.x' + + - name: Install maturin + run: pip install maturin==1.8.3 + + - name: Build wheel + working-directory: packages/edge/rust/py + run: maturin build --release + + - name: Upload wheel artifact + uses: actions/upload-artifact@v4 + with: + name: edge-python-wheel + path: packages/edge/rust/target/wheels/*.whl + if-no-files-found: error + + js-build-test: + name: JS build & test + needs: [build-wasm] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Download WASM artifact + uses: actions/download-artifact@v4 + with: + name: edge-wasm + path: packages/edge/js/src/rust-bridge/pkg/ + + - name: Install JS dependencies + working-directory: packages/edge/js + run: npm install + + - name: Build TypeScript + working-directory: packages/edge/js + run: npm run build + + - name: Run JS tests + working-directory: packages/edge/js + run: npm run test:ci + + - name: Upload npm package + uses: actions/upload-artifact@v4 + with: + name: edge-npm-package + path: packages/edge/js/dist/ + if-no-files-found: error + + python-tests: + name: Python tests + needs: [build-python] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13.x' + + - name: Install dependencies + working-directory: packages/edge + run: pip install -r full_requirements.txt + + - name: Download Python wheel + uses: actions/download-artifact@v4 + with: + name: edge-python-wheel + path: dist/ + + - name: Install compiled Rust extension + run: pip install --force-reinstall --no-deps dist/*.whl + + - name: Install package (editable) + working-directory: packages/edge + run: pip install -e . + + - name: Run tests + working-directory: packages/edge + run: pytest tests -v + + publish-npm: + name: Publish npm package + needs: [rust-tests, js-build-test, build-napi, build-wasm] + if: startsWith(github.ref, 'refs/tags/edge-') + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + + # Download the built JS package (dist/) + - name: Download npm package artifact + uses: actions/download-artifact@v4 + with: + name: edge-npm-package + path: packages/edge/js/dist/ + + # Download WASM binary and copy into dist/ + - name: Download WASM artifact + uses: actions/download-artifact@v4 + with: + name: edge-wasm + path: /tmp/wasm-artifact/ + + - name: Copy WASM binary into dist + run: | + mkdir -p packages/edge/js/dist/wasm + cp /tmp/wasm-artifact/*.wasm packages/edge/js/dist/wasm/ || true + cp /tmp/wasm-artifact/*.js packages/edge/js/dist/wasm/ || true + ls -la packages/edge/js/dist/wasm/ + + # Download NAPI binaries and copy into dist/ so they're included in the publish + - name: Download NAPI artifacts + uses: actions/download-artifact@v4 + with: + pattern: edge-napi-* + path: /tmp/napi-artifacts/ + + - name: Copy NAPI binaries into dist + run: | + mkdir -p packages/edge/js/dist/native + for dir in /tmp/napi-artifacts/edge-napi-*/; do + cp "$dir"/*.node packages/edge/js/dist/native/ || true + done + ls -la packages/edge/js/dist/native/ + + - name: Publish to npm + working-directory: packages/edge/js + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-python: + name: Publish Python package to PyPI + needs: [rust-tests, python-tests, build-python] + if: startsWith(github.ref, 'refs/tags/edge-') + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13.x' + + - name: Download Python wheel + uses: actions/download-artifact@v4 + with: + name: edge-python-wheel + path: dist/ + + - name: Publish to PyPI + env: + TWINE_REPOSITORY_URL: ${{ secrets.PYPI_OFFICIAL_UPLOAD_URL }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + pip install twine + python -m twine upload --skip-existing dist/* + + publish-release: + name: Upload release assets + needs: [build-wasm, build-napi, build-python] + if: startsWith(github.ref, 'refs/tags/edge-') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + pattern: edge-* + + - name: Create archives + run: | + tar czf edge-wasm.tar.gz -C edge-wasm . + for dir in edge-napi-*; do + tar czf "${dir}.tar.gz" -C "$dir" . + done + tar czf edge-python-wheel.tar.gz -C edge-python-wheel . + + - name: Upload to release + uses: softprops/action-gh-release@v2 + with: + files: | + edge-wasm.tar.gz + edge-napi-*.tar.gz + edge-python-wheel.tar.gz diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 666e4dc062..9c03f0f555 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,7 @@ jobs: - packages/async_channel - packages/backtesting - packages/commons + - packages/edge # tests run in Python-fallback mode (no Rust); edge.yml tests Rust - packages/evaluators - packages/node - packages/flow diff --git a/.gitignore b/.gitignore index fd35a23ab6..beec629037 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,18 @@ installer/ # ai analysis/ + +# octobot_edge — Rust compiled artifacts +packages/edge/octobot_edge/_native*.so +packages/edge/octobot_edge/_native*.pyd +packages/edge/octobot_edge/_native*.dylib +packages/edge/rust/target/ +packages/edge/js/dist/ +packages/edge/js/node_modules/ +packages/edge/js/src/rust-bridge/pkg/ +packages/edge/js/src/rust-bridge/*.node +packages/edge/js/dist/native/*.node +packages/edge/rust/napi/*.node +packages/edge/js/src/rust-bridge/native.js +packages/edge/js/src/rust-bridge/native.d.ts +packages/edge/js/*.tsbuildinfo diff --git a/BUILD b/BUILD index 68974f6b6f..87c68d0d5b 100644 --- a/BUILD +++ b/BUILD @@ -30,6 +30,7 @@ PACKAGE_SOURCES = [ "packages/async_channel:async_channel", "packages/backtesting:octobot_backtesting", "packages/commons:octobot_commons", + "packages/edge:octobot_edge", "packages/evaluators:octobot_evaluators", "packages/node:octobot_node", "packages/flow:octobot_flow", @@ -43,6 +44,7 @@ PACKAGE_SOURCES = [ PACKAGE_REQS = [ "packages/backtesting:reqs", "packages/commons:reqs", + "packages/edge:reqs", "packages/evaluators:reqs", "packages/node:reqs", "packages/sync:reqs", diff --git a/packages/edge/BUILD b/packages/edge/BUILD new file mode 100644 index 0000000000..b5867da4dc --- /dev/null +++ b/packages/edge/BUILD @@ -0,0 +1,45 @@ +python_requirements(name="reqs") +python_requirements(name="full_reqs", source="full_requirements.txt") + +python_sources(name="octobot_edge", sources=["octobot_edge/**/*.py", "octobot_edge/**/*.pyi"]) + +files( + name="test_data", + sources=["tests/static/**/*"], +) + +python_tests( + name="tests", + sources=["tests/**/test_*.py"], + dependencies=[ + ":octobot_edge", + ":reqs", + ":full_reqs", + "//:dev_reqs", + ":test_data", + ], +) + +files( + name="native_extension", + sources=["octobot_edge/_native*.so", "octobot_edge/_native*.pyd", "octobot_edge/_native*.dylib"], +) + +python_distribution( + name="OctoBot-Edge", + dependencies=[":octobot_edge", ":reqs", ":native_extension"], + provides=python_artifact( + name="octobot-edge", + version="0.1.0", + url="https://github.com/Drakkar-Software/OctoBot", + license="GPL-3.0", + author="Drakkar-Software", + author_email="contact@drakkar.software", + description="Universal CCXT wrapper for OctoBot — Python + JS, Rust-accelerated", + long_description_file="ARCHITECTURE.md", + long_description_content_type="text/markdown", + ), + generate_setup=True, + sdist=True, + wheel=True, +) diff --git a/packages/edge/README.md b/packages/edge/README.md new file mode 100644 index 0000000000..32de7c7da0 --- /dev/null +++ b/packages/edge/README.md @@ -0,0 +1,281 @@ +# OctoBot Edge + +Universal CCXT wrapper with Rust-accelerated crypto and polyfills. Available as both a **Python package** (`octobot-edge`) and an **npm package** (`@octobot/edge`), sharing a single Rust core for HMAC, hashing, buffer operations, and HTTP fetch. + +Formerly `edge-ccxt` — all code from that repository is now integrated here. + +## Concept + +CCXT requires crypto (HMAC, hashing), Buffer, and fetch operations that are unavailable or slow on some platforms. OctoBot Edge provides all of these through a compiled Rust core, enabling CCXT to run on mobile (iOS/Android), browser, and server with optimal performance. + +Instead of forking CCXT, it monkey-patches `exchange.hmac()` at runtime and provides globalThis polyfills — keeping CCXT up to date while accelerating the hot path. + +### Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Rust Core (rust/core/) │ +│ HMAC · Hash · Random · Buffer · Fetch · Encoding │ +└──────────┬──────────┬──────────┬──────────┬─────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ + PyO3 NAPI WASM UniFFI + (Python) (Node/Bun) (Browser) (iOS/Android) +``` + +When the Rust bridge is unavailable, both Python and JS gracefully fall back to their respective standard library implementations. + +## Polyfill Coverage + +| Polyfill | Node/Bun | Browser (WASM) | React Native (UniFFI) | Python | +|----------------------|----------|----------------|-----------------------|--------| +| HMAC (sha256/512) | Rust | Rust | Rust | Rust | +| Hash (sha*/md5) | Rust | Rust | Rust | Rust | +| randomBytes | Rust | Rust | Rust | - | +| Buffer | Native | Rust-backed | Rust-backed | - | +| TextEncoder/Decoder | Native | Native | JS polyfill | - | +| fetch | Native | Native | Rust (reqwest) | - | +| URLSearchParams | Native | Native | JS polyfill | - | +| btoa/atob | Native | Native | JS polyfill | - | +| Exchange signing | Rust | Rust | Rust | Rust | + +## Supported Runtimes + +| Runtime | Rust Bridge | Status | +|----------------|-----------------|-----------| +| Python ≥ 3.10 | PyO3 native ext | Full | +| Node.js ≥ 20 | NAPI native | Full | +| Bun ≥ 1.0 | NAPI native | Full | +| Deno (local) | WASM | Full | +| Browser | WASM | Full | +| React Native | UniFFI (iOS/Android) | Full | + +## Installation + +### Python + +```bash +pip install octobot-edge +``` + +From source (requires Rust toolchain): + +```bash +cd packages/edge +pip install -r requirements.txt +cd rust/py && maturin develop --release && cd ../.. +pip install -e . +``` + +### JavaScript / TypeScript + +```bash +npm install @octobot/edge +``` + +## Usage + +### Python + +```python +from octobot_edge.exchange.client import create_exchange + +# Sync exchange +exchange = create_exchange("binance", { + "apiKey": "your_key", + "secret": "your_secret", +}) + +# Async exchange +exchange = create_exchange("binance", { + "apiKey": "your_key", + "secret": "your_secret", +}, async_mode=True) + +ticker = await exchange.fetch_ticker("BTC/USDT") +``` + +Direct crypto access: + +```python +from octobot_edge.crypto.hmac import hmac_sha256 + +signature = hmac_sha256(b"secret_key", b"message") +print(signature.hex()) +``` + +### Node.js / Bun + +```typescript +import { init, createExchange } from "@octobot/edge" + +// Initialize once — auto-detects NAPI (Node/Bun) or WASM (browser/Deno) +await init() + +const binance = createExchange("binance", { + apiKey: "your_key", + secret: "your_secret", +}) + +const ticker = await binance.fetchTicker("BTC/USDT") +``` + +### Browser + +```typescript +import { init, createExchange } from "@octobot/edge" + +// Loads WASM + installs crypto/Buffer polyfills automatically +await init() + +const exchange = createExchange("binance", { + proxy: "https://your-proxy.example.com/proxy/", +}) +``` + +### React Native (iOS/Android) + +```typescript +// 1. Import mobile polyfills BEFORE ccxt (installs crypto, Buffer, fetch, etc.) +import "@octobot/edge/polyfills/mobile" + +// 2. Then use normally +import { init, createExchange } from "@octobot/edge" +await init() +const exchange = createExchange("binance", { apiKey: "...", secret: "..." }) +``` + +### Deno + +```typescript +import { init, createExchange } from "npm:@octobot/edge" +await init() +``` + +## Testing + +### Python tests (81 tests) + +```bash +cd packages/edge +pip install -r full_requirements.txt +pytest tests -v +``` + +### JavaScript tests + +```bash +cd packages/edge/js +npm install +npm test # All tests +npm run test:binance # Live Binance API integration tests +``` + +### Rust tests + +```bash +cd packages/edge/rust +cargo test +``` + +## Building from Source + +### Prerequisites + +```bash +# Rust toolchain +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +rustup target add wasm32-unknown-unknown + +# Build tools +cargo install wasm-pack +npm install -g @napi-rs/cli +pip install maturin + +# For React Native (optional) +cargo install cargo-ndk +npm install -g uniffi-bindgen-react-native +rustup target add aarch64-apple-ios aarch64-linux-android +``` + +### Build all targets + +```bash +cd packages/edge + +# 1. Python extension +cd rust/py && maturin develop --release && cd ../.. + +# 2. WASM (browser + Deno) +cd js && npm run build:wasm && cd .. + +# 3. NAPI (Node.js + Bun) +cd js && npm run build:napi && cd .. + +# 4. Mobile (React Native) +cd js && npm run build:mobile:ios && cd .. # iOS +cd js && npm run build:mobile:android && cd .. # Android + +# 5. TypeScript → JavaScript +cd js && npm run build && cd .. +``` + +## Project Layout + +``` +packages/edge/ +├── octobot_edge/ # Python package +│ ├── exchange/ # Exchange factory + normalization +│ ├── crypto/ # HMAC with Rust fallback +│ └── _native.pyi # Type stubs for Rust extension +├── tests/ # Python tests +│ └── static/ # Test fixtures (API response samples) +├── rust/ # Rust workspace +│ ├── core/ # Shared crypto/buffer/fetch logic (no FFI) +│ ├── wasm/ # wasm-bindgen target (browser) +│ ├── napi/ # napi-rs target (Node/Bun) +│ ├── py/ # PyO3/maturin target (Python) +│ └── mobile/ # UniFFI target (iOS/Android) +├── js/ # npm package (@octobot/edge) +│ ├── src/ +│ │ ├── polyfills/ +│ │ │ ├── browser.ts # WASM crypto + Buffer polyfill +│ │ │ ├── mobile.ts # UniFFI crypto + Buffer + fetch + URLSearchParams +│ │ │ ├── encoding.ts # TextEncoder/Decoder for Hermes +│ │ │ ├── crypto.ts # Basic crypto polyfill +│ │ │ ├── detect.ts # Runtime detection +│ │ │ └── node.ts # Node.js test polyfills +│ │ ├── rust-bridge/ # NAPI + WASM loaders +│ │ └── exchange/ # CCXT hmac() patching +│ ├── tests/ # JS tests +│ ├── scripts/ # Build helpers +│ └── ubrn.config.yaml # UniFFI React Native config +├── BUILD # Pants build targets +├── pyproject.toml # Python package config +├── requirements.txt # Python runtime deps +└── full_requirements.txt # Python dev/test deps +``` + +## Design Decisions + +1. **Shared Rust core** — All crypto, buffer, and fetch operations are implemented once in Rust and exposed via PyO3, NAPI, wasm-bindgen, and UniFFI. No duplicated logic. + +2. **Full polyfill coverage** — Beyond HMAC, provides Buffer, fetch, URLSearchParams, btoa/atob, process, and DOMException polyfills for React Native where Node.js APIs are absent. + +3. **Graceful fallback** — Python falls back to `hmac`/`hashlib` from stdlib. JS falls back to native `crypto`. The package always works, Rust just makes it faster. + +4. **CCXT patching, not forking** — Monkey-patching `exchange.hmac()` at runtime means CCXT stays on the latest version with full exchange support. + +5. **No embedded JS interpreter** — Unlike alternatives that bundle QuickJS/Hermes (5+ MB), CCXT runs in the host JS runtime. Only crypto/buffer/fetch are delegated to Rust, resulting in 75% smaller bundles. + +## CI/CD + +The workflow at `.github/workflows/edge.yml` runs on changes to `packages/edge/`: + +- **rust-tests** — `cargo test` on Linux, macOS, Windows +- **build-wasm** — WASM target build + artifact upload +- **build-napi** — Native addons for Linux, macOS, Windows +- **build-python** — Maturin wheel build +- **js-build-test** — TypeScript build + Jest tests +- **python-tests** — pytest with compiled Rust extension +- **publish-npm** / **publish-python** — On `edge-*` tags diff --git a/packages/edge/full_requirements.txt b/packages/edge/full_requirements.txt new file mode 100644 index 0000000000..1e858faa7b --- /dev/null +++ b/packages/edge/full_requirements.txt @@ -0,0 +1,3 @@ +ccxt>=4.0.0 +pytest>=8.0 +pytest-asyncio>=0.23 diff --git a/packages/edge/js/biome.json b/packages/edge/js/biome.json new file mode 100644 index 0000000000..bf73b51366 --- /dev/null +++ b/packages/edge/js/biome.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + } +} diff --git a/packages/edge/js/esbuild.mjs b/packages/edge/js/esbuild.mjs new file mode 100644 index 0000000000..e4ed529371 --- /dev/null +++ b/packages/edge/js/esbuild.mjs @@ -0,0 +1,58 @@ +import { build } from "esbuild"; + +// Bundle ccxt for browser — used by both mobile (React Native) and browser builds. +// Node.js built-ins are shimmed away since we provide crypto/fetch/Buffer +// as Rust-backed polyfills patched onto globalThis. +const NODE_BUILTINS = [ + "assert", "buffer", "crypto", "events", "http", "https", + "net", "stream", "tls", "url", "util", "zlib", "dns", + "node:assert", "node:buffer", "node:crypto", "node:events", + "node:http", "node:https", "node:net", "node:stream", + "node:tls", "node:url", "node:util", "node:zlib", "node:dns", + "http-proxy-agent", "socks-proxy-agent", +]; + +const EMPTY_MODULE = "export default {}; export const PassThrough = class {}; export const pipeline = () => {}; export const Buffer = { from: () => new Uint8Array(), alloc: () => new Uint8Array(), isBuffer: () => false }; export const types = {}; export const deprecate = (fn) => fn; export const promisify = (fn) => fn; export const inherits = () => {}; export const isIP = () => 0; export const format = () => ''; export const once = async () => [];"; + +const nodeShimPlugin = { + name: "node-builtins-shim", + setup(b) { + const builtinFilter = new RegExp( + "^(" + NODE_BUILTINS.map((n) => n.replace(":", "\\:")).join("|") + ")$", + ); + b.onResolve({ filter: builtinFilter }, (args) => ({ + path: args.path, + namespace: "node-shim", + })); + + b.onResolve({ filter: /^protobufjs/ }, (args) => ({ + path: args.path, + namespace: "node-shim", + })); + + b.onLoad({ filter: /.*/, namespace: "node-shim" }, () => ({ + contents: EMPTY_MODULE, + loader: "js", + })); + }, +}; + +await build({ + entryPoints: ["node_modules/ccxt/dist/cjs/ccxt.js"], + bundle: true, + outfile: "dist/ccxt.bundle.js", + format: "cjs", + platform: "neutral", + target: "es2020", + mainFields: ["browser", "module", "main"], + conditions: ["browser", "import", "default"], + define: { + "process.env.NODE_ENV": '"production"', + "process.version": '"v20.0.0"', + "process.browser": "true", + global: "globalThis", + }, + plugins: [nodeShimPlugin], + minify: true, + logLevel: "info", +}); diff --git a/packages/edge/js/jest.config.js b/packages/edge/js/jest.config.js new file mode 100644 index 0000000000..77241bd933 --- /dev/null +++ b/packages/edge/js/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + testTimeout: 30_000, + testMatch: ['**/tests/**/*.test.ts'], + transform: { + '^.+\\.tsx?$': ['ts-jest', { useESM: true }], + }, + extensionsToTreatAsEsm: ['.ts'], + verbose: true, +}; diff --git a/packages/edge/js/package-lock.json b/packages/edge/js/package-lock.json new file mode 100644 index 0000000000..9974fb4a58 --- /dev/null +++ b/packages/edge/js/package-lock.json @@ -0,0 +1,7812 @@ +{ + "name": "@octobot/edge", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@octobot/edge", + "version": "0.1.0", + "dependencies": { + "ccxt": "*" + }, + "devDependencies": { + "@biomejs/biome": "^2.0.0", + "@jest/globals": "^29.0.0", + "@napi-rs/cli": "^3.0.0", + "esbuild": "^0.24.0", + "jest": "^29.0.0", + "ts-jest": "^29.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8", + "uniffi-bindgen-react-native": "*", + "wasm-pack": "^0.13" + }, + "optionalDependencies": { + "@octobot/edge-napi-darwin-arm64": "0.1.0", + "@octobot/edge-napi-linux-x64-gnu": "0.1.0", + "@octobot/edge-napi-win32-x64-msvc": "0.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@biomejs/biome": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.8.tgz", + "integrity": "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.8", + "@biomejs/cli-darwin-x64": "2.4.8", + "@biomejs/cli-linux-arm64": "2.4.8", + "@biomejs/cli-linux-arm64-musl": "2.4.8", + "@biomejs/cli-linux-x64": "2.4.8", + "@biomejs/cli-linux-x64-musl": "2.4.8", + "@biomejs/cli-win32-arm64": "2.4.8", + "@biomejs/cli-win32-x64": "2.4.8" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.8.tgz", + "integrity": "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.8.tgz", + "integrity": "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.8.tgz", + "integrity": "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.8.tgz", + "integrity": "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.8.tgz", + "integrity": "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.8.tgz", + "integrity": "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.8.tgz", + "integrity": "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.8.tgz", + "integrity": "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.4.tgz", + "integrity": "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.2.tgz", + "integrity": "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.10.tgz", + "integrity": "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.7.tgz", + "integrity": "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.10.tgz", + "integrity": "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/external-editor": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.10.tgz", + "integrity": "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.4.tgz", + "integrity": "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.4.tgz", + "integrity": "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.10.tgz", + "integrity": "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.10.tgz", + "integrity": "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.10.tgz", + "integrity": "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.3.2.tgz", + "integrity": "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.2", + "@inquirer/confirm": "^6.0.10", + "@inquirer/editor": "^5.0.10", + "@inquirer/expand": "^5.0.10", + "@inquirer/input": "^5.0.10", + "@inquirer/number": "^4.0.10", + "@inquirer/password": "^5.0.10", + "@inquirer/rawlist": "^5.2.6", + "@inquirer/search": "^4.1.6", + "@inquirer/select": "^5.1.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.6.tgz", + "integrity": "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.6.tgz", + "integrity": "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.2.tgz", + "integrity": "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.4.tgz", + "integrity": "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/cli": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.5.1.tgz", + "integrity": "sha512-XBfLQRDcB3qhu6bazdMJsecWW55kR85l5/k0af9BIBELXQSsCFU0fzug7PX8eQp6vVdm7W/U3z6uP5WmITB2Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@napi-rs/cross-toolchain": "^1.0.3", + "@napi-rs/wasm-tools": "^1.0.1", + "@octokit/rest": "^22.0.1", + "clipanion": "^4.0.0-rc.4", + "colorette": "^2.0.20", + "emnapi": "^1.7.1", + "es-toolkit": "^1.41.0", + "js-yaml": "^4.1.0", + "obug": "^2.0.0", + "semver": "^7.7.3", + "typanion": "^3.14.0" + }, + "bin": { + "napi": "dist/cli.js", + "napi-raw": "cli.mjs" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/runtime": "^1.7.1" + }, + "peerDependenciesMeta": { + "@emnapi/runtime": { + "optional": true + } + } + }, + "node_modules/@napi-rs/cross-toolchain": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz", + "integrity": "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==", + "dev": true, + "license": "MIT", + "workspaces": [ + ".", + "arm64/*", + "x64/*" + ], + "dependencies": { + "@napi-rs/lzma": "^1.4.5", + "@napi-rs/tar": "^1.1.0", + "debug": "^4.4.1" + }, + "peerDependencies": { + "@napi-rs/cross-toolchain-arm64-target-aarch64": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-armv7": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-ppc64le": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-s390x": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-x86_64": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-aarch64": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-armv7": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-ppc64le": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-s390x": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-x86_64": "^1.0.3" + }, + "peerDependenciesMeta": { + "@napi-rs/cross-toolchain-arm64-target-aarch64": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-armv7": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-ppc64le": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-s390x": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-x86_64": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-aarch64": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-armv7": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-ppc64le": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-s390x": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-x86_64": { + "optional": true + } + } + }, + "node_modules/@napi-rs/lzma": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma/-/lzma-1.4.5.tgz", + "integrity": "sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/lzma-android-arm-eabi": "1.4.5", + "@napi-rs/lzma-android-arm64": "1.4.5", + "@napi-rs/lzma-darwin-arm64": "1.4.5", + "@napi-rs/lzma-darwin-x64": "1.4.5", + "@napi-rs/lzma-freebsd-x64": "1.4.5", + "@napi-rs/lzma-linux-arm-gnueabihf": "1.4.5", + "@napi-rs/lzma-linux-arm64-gnu": "1.4.5", + "@napi-rs/lzma-linux-arm64-musl": "1.4.5", + "@napi-rs/lzma-linux-ppc64-gnu": "1.4.5", + "@napi-rs/lzma-linux-riscv64-gnu": "1.4.5", + "@napi-rs/lzma-linux-s390x-gnu": "1.4.5", + "@napi-rs/lzma-linux-x64-gnu": "1.4.5", + "@napi-rs/lzma-linux-x64-musl": "1.4.5", + "@napi-rs/lzma-wasm32-wasi": "1.4.5", + "@napi-rs/lzma-win32-arm64-msvc": "1.4.5", + "@napi-rs/lzma-win32-ia32-msvc": "1.4.5", + "@napi-rs/lzma-win32-x64-msvc": "1.4.5" + } + }, + "node_modules/@napi-rs/lzma-android-arm-eabi": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.4.5.tgz", + "integrity": "sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-android-arm64": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.4.5.tgz", + "integrity": "sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-darwin-arm64": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.4.5.tgz", + "integrity": "sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-darwin-x64": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.4.5.tgz", + "integrity": "sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-freebsd-x64": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.4.5.tgz", + "integrity": "sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-arm-gnueabihf": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.4.5.tgz", + "integrity": "sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-gnu": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.4.5.tgz", + "integrity": "sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-musl": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.4.5.tgz", + "integrity": "sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-ppc64-gnu": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.4.5.tgz", + "integrity": "sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-riscv64-gnu": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.4.5.tgz", + "integrity": "sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-s390x-gnu": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.4.5.tgz", + "integrity": "sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.4.5.tgz", + "integrity": "sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-musl": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.4.5.tgz", + "integrity": "sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-wasm32-wasi": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.4.5.tgz", + "integrity": "sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/lzma-win32-arm64-msvc": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.4.5.tgz", + "integrity": "sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-win32-ia32-msvc": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.4.5.tgz", + "integrity": "sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/lzma-win32-x64-msvc": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.4.5.tgz", + "integrity": "sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar/-/tar-1.1.0.tgz", + "integrity": "sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/tar-android-arm-eabi": "1.1.0", + "@napi-rs/tar-android-arm64": "1.1.0", + "@napi-rs/tar-darwin-arm64": "1.1.0", + "@napi-rs/tar-darwin-x64": "1.1.0", + "@napi-rs/tar-freebsd-x64": "1.1.0", + "@napi-rs/tar-linux-arm-gnueabihf": "1.1.0", + "@napi-rs/tar-linux-arm64-gnu": "1.1.0", + "@napi-rs/tar-linux-arm64-musl": "1.1.0", + "@napi-rs/tar-linux-ppc64-gnu": "1.1.0", + "@napi-rs/tar-linux-s390x-gnu": "1.1.0", + "@napi-rs/tar-linux-x64-gnu": "1.1.0", + "@napi-rs/tar-linux-x64-musl": "1.1.0", + "@napi-rs/tar-wasm32-wasi": "1.1.0", + "@napi-rs/tar-win32-arm64-msvc": "1.1.0", + "@napi-rs/tar-win32-ia32-msvc": "1.1.0", + "@napi-rs/tar-win32-x64-msvc": "1.1.0" + } + }, + "node_modules/@napi-rs/tar-android-arm-eabi": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.0.tgz", + "integrity": "sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-android-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.0.tgz", + "integrity": "sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.0.tgz", + "integrity": "sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-freebsd-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.0.tgz", + "integrity": "sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm-gnueabihf": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.0.tgz", + "integrity": "sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm64-gnu": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.0.tgz", + "integrity": "sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm64-musl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.0.tgz", + "integrity": "sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-ppc64-gnu": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.0.tgz", + "integrity": "sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-s390x-gnu": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.0.tgz", + "integrity": "sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-x64-gnu": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.0.tgz", + "integrity": "sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-x64-musl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.0.tgz", + "integrity": "sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-wasm32-wasi": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.0.tgz", + "integrity": "sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/tar-win32-arm64-msvc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.0.tgz", + "integrity": "sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-win32-ia32-msvc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.0.tgz", + "integrity": "sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-win32-x64-msvc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.0.tgz", + "integrity": "sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/wasm-tools": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools/-/wasm-tools-1.0.1.tgz", + "integrity": "sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/wasm-tools-android-arm-eabi": "1.0.1", + "@napi-rs/wasm-tools-android-arm64": "1.0.1", + "@napi-rs/wasm-tools-darwin-arm64": "1.0.1", + "@napi-rs/wasm-tools-darwin-x64": "1.0.1", + "@napi-rs/wasm-tools-freebsd-x64": "1.0.1", + "@napi-rs/wasm-tools-linux-arm64-gnu": "1.0.1", + "@napi-rs/wasm-tools-linux-arm64-musl": "1.0.1", + "@napi-rs/wasm-tools-linux-x64-gnu": "1.0.1", + "@napi-rs/wasm-tools-linux-x64-musl": "1.0.1", + "@napi-rs/wasm-tools-wasm32-wasi": "1.0.1", + "@napi-rs/wasm-tools-win32-arm64-msvc": "1.0.1", + "@napi-rs/wasm-tools-win32-ia32-msvc": "1.0.1", + "@napi-rs/wasm-tools-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/wasm-tools-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.0.1.tgz", + "integrity": "sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.0.1.tgz", + "integrity": "sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "Python-2.0" + }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/binary-install": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.1.2.tgz", + "integrity": "sha512-ZS2cqFHPZOy4wLxvzqfQvDjCOifn+7uCPqNmYRIBM/03+yllON+4fNnsD0VJdW0p97y+E+dTRNPStWNqMBq+9g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.26.1", + "rimraf": "^3.0.2", + "tar": "^6.1.11" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccxt": { + "version": "4.5.44", + "resolved": "https://registry.npmjs.org/ccxt/-/ccxt-4.5.44.tgz", + "integrity": "sha512-cumeM+Mmb2Gj103G7q3oyjLoJ6Wf8JGwdVZO2y9QSg2RVHWgTSTcZehJt4Etj44k39X82tVPSI4+w3zvHVgAjQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "ws": "^8.8.1" + }, + "engines": { + "node": ">=15.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clipanion": { + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.4.tgz", + "integrity": "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ], + "dependencies": { + "typanion": "^3.8.0" + }, + "peerDependencies": { + "typanion": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emnapi": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/emnapi/-/emnapi-1.9.1.tgz", + "integrity": "sha512-s4RbfzgbYg9cWBZXJT6LazImJQ5p+F+LyTsCWQJXbGVdPmtCtdlwqd0Oiv3O51KyYV/Hq58xszaQ/l153tK6Uw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "node-addon-api": ">= 6.1.0" + }, + "peerDependenciesMeta": { + "node-addon-api": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "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" + } + ], + "license": "MIT", + "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/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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, + "license": "Apache-2.0" + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": 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, + "license": "MIT", + "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/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/tsup/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, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/typanion": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/typanion/-/typanion-3.14.0.tgz", + "integrity": "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ] + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uniffi-bindgen-react-native": { + "version": "0.30.0-1", + "resolved": "https://registry.npmjs.org/uniffi-bindgen-react-native/-/uniffi-bindgen-react-native-0.30.0-1.tgz", + "integrity": "sha512-rffyNPTQ2qFx7deaLXYVBhXcy4pDrjtEc28Pc1Rwt532IGEQquT7eNvvLKIgX6G5xODi6Yfni1zFs5u+O2QL1A==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "ubrn": "bin/cli.cjs", + "uniffi-bindgen-react-native": "bin/cli.cjs" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wasm-pack": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.13.1.tgz", + "integrity": "sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "binary-install": "^1.0.1" + }, + "bin": { + "wasm-pack": "run.js" + } + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "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": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/edge/js/package.json b/packages/edge/js/package.json new file mode 100644 index 0000000000..181d9e9e22 --- /dev/null +++ b/packages/edge/js/package.json @@ -0,0 +1,83 @@ +{ + "name": "@octobot/edge", + "version": "0.1.0", + "type": "module", + "description": "Universal CCXT wrapper — Node, Bun, Deno, browser, React Native (iOS/Android)", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "react-native": "./dist/rn/index.js", + "browser": "./dist/browser/index.mjs", + "bun": "./dist/esm/index.mjs", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + }, + "./wasm": { + "types": "./dist/wasm/octobot_edge_wasm.d.ts", + "default": "./dist/wasm/octobot_edge_wasm.js" + }, + "./polyfills/node": { + "import": "./dist/esm/polyfills/node.mjs", + "require": "./dist/cjs/polyfills/node.cjs", + "default": "./dist/esm/polyfills/node.mjs" + }, + "./polyfills/mobile": { + "react-native": "./dist/rn/polyfills/mobile.js", + "default": "./dist/esm/polyfills/mobile.mjs" + }, + "./polyfills/browser": { + "import": "./dist/esm/polyfills/browser.mjs", + "default": "./dist/esm/polyfills/browser.mjs" + }, + "./polyfills/crypto": { + "import": "./dist/esm/polyfills/crypto.mjs", + "require": "./dist/cjs/polyfills/crypto.cjs", + "default": "./dist/esm/polyfills/crypto.mjs" + } + }, + "files": [ + "src", + "dist", + "dist/native", + "ios", + "android", + "ubrn.config.yaml", + "README.md" + ], + "scripts": { + "build": "npm run build:all", + "build:js": "tsup", + "build:wasm": "cd ../rust/wasm && wasm-pack build --target bundler --out-dir ../../js/src/rust-bridge/pkg", + "build:napi": "cd ../rust/napi && napi build --platform --release --js ../../js/src/rust-bridge/native.js --dts ../../js/src/rust-bridge/native.d.ts", + "build:mobile:ios": "uniffi-bindgen-react-native build --config ubrn.config.yaml --release --and-generate && node scripts/postprocess-uniffi.js", + "build:mobile:android": "uniffi-bindgen-react-native build --config ubrn.config.yaml --release --and-generate --targets aarch64-linux-android armv7-linux-androideabi x86_64-linux-android && node scripts/postprocess-uniffi.js", + "build:ccxt": "node esbuild.mjs", + "build:all": "npm run build:wasm && npm run build:napi && npm run build:js", + "sync:wasm": "node scripts/sync-wasm-artifacts.js", + "lint": "biome check --write --unsafe src/", + "test": "node --experimental-vm-modules node_modules/.bin/jest", + "test:ci": "jest --ci", + "test:binance": "jest --ci tests/binance.integration.test.ts" + }, + "dependencies": { + "ccxt": ">=4.0.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.0.0", + "@napi-rs/cli": "^3.0.0", + "esbuild": "^0.24.0", + "jest": "^29.0.0", + "ts-jest": "^29.0.0", + "@jest/globals": "^29.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8", + "wasm-pack": "^0.13", + "uniffi-bindgen-react-native": "*" + }, + "optionalDependencies": { + "@octobot/edge-napi-linux-x64-gnu": "0.1.0", + "@octobot/edge-napi-darwin-arm64": "0.1.0", + "@octobot/edge-napi-win32-x64-msvc": "0.1.0" + } +} diff --git a/packages/edge/js/scripts/postprocess-uniffi.js b/packages/edge/js/scripts/postprocess-uniffi.js new file mode 100644 index 0000000000..332a982271 --- /dev/null +++ b/packages/edge/js/scripts/postprocess-uniffi.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); + +const replacements = [ + { + // Use get() instead of getEnforcing() so the module returns null when not + // registered (e.g. Expo Go, or a build where the native lib was not linked) + file: path.join(root, 'src', 'NativeOctobotEdgeMobile.ts'), + from: /TurboModuleRegistry\.getEnforcing any +} + +let enc: TextEncoder | null = null + +function getEncoder(): TextEncoder { + if (!enc) enc = new TextEncoder() + return enc +} + +/** Convert a string or binary value to Uint8Array, preserving byte fidelity. */ +function toBytes(value: string | Uint8Array | ArrayBuffer): Uint8Array { + if (value instanceof Uint8Array) return value + if (value instanceof ArrayBuffer) return new Uint8Array(value) + // Use TextEncoder for standard strings. CCXT secrets/requests are typically + // ASCII, so UTF-8 encoding matches the expected byte representation. + return getEncoder().encode(value) +} + +export function patchExchange(exchange: PatchableExchange, rustBridge: RustBridge | null): void { + if (!rustBridge) return + + const original = exchange.hmac.bind(exchange) + + exchange.hmac = function ( + request: string | Uint8Array, + secret: string | Uint8Array, + hash: string = "sha256", + digest: string = "hex", + ): string { + // Only accelerate hex-encoded HMAC; fall back for other digest formats + if (digest === "hex") { + const algo = typeof hash === "string" ? hash.toLowerCase() : "" + if (algo === "sha256") { + return rustBridge.hmacSha256Hex(toBytes(secret), toBytes(request)) + } + if (algo === "sha512") { + return rustBridge.hmacSha512Hex(toBytes(secret), toBytes(request)) + } + } + return original(request, secret, hash, digest) + } +} diff --git a/packages/edge/js/src/index.ts b/packages/edge/js/src/index.ts new file mode 100644 index 0000000000..d9be731b46 --- /dev/null +++ b/packages/edge/js/src/index.ts @@ -0,0 +1,87 @@ +import ccxt from "ccxt" +import { detectRuntime } from "./polyfills/detect" +import { installEncodingPolyfill } from "./polyfills/encoding" +import { installCryptoPolyfill } from "./polyfills/crypto" +import { installBrowserPolyfills } from "./polyfills/browser" +import { loadNative } from "./rust-bridge/native" +import { loadWasm } from "./rust-bridge/wasm" +import { patchExchange } from "./exchange/patch" +import type { RustBridge } from "./rust-bridge/types" + +let _bridge: RustBridge | null = null +let _initPromise: Promise | null = null +let _initResolved = false + +/** + * Initialize the Rust bridge. Call once before creating exchanges. + * Automatically selects NAPI (Node/Bun) or WASM (browser/Deno). + * Safe to call concurrently — only runs once. + * Must be awaited before calling createExchange(). + */ +export async function init(): Promise { + if (_initPromise) return _initPromise + _initPromise = _doInit() + return _initPromise +} + +async function _doInit(): Promise { + const runtime = detectRuntime() + + // Install encoding polyfills early for environments without TextEncoder (e.g. Hermes/RN) + if (runtime === "react-native") { + installEncodingPolyfill() + } + + if (runtime === "node" || runtime === "bun") { + _bridge = await loadNative() + } + + if (!_bridge) { + _bridge = await loadWasm() + } + + // Install full polyfills (crypto, Buffer, etc.) for environments that need them + if (_bridge) { + if (runtime === "browser" || runtime === "deno") { + await installBrowserPolyfills(_bridge) + } else if (runtime === "react-native") { + // React Native polyfills are loaded via separate import: + // import '@octobot/edge/polyfills/mobile' + // The mobile.ts module self-installs on import (uses UniFFI bindings). + // Here we just install the basic crypto polyfill as a fallback. + installCryptoPolyfill(_bridge) + } + } else { + console.warn( + "@octobot/edge: no Rust bridge available (NAPI and WASM both failed to load). " + + "Exchange signing will use the default (slower) ccxt implementation.", + ) + } + + _initResolved = true +} + +/** + * Create a ccxt exchange instance with Rust-accelerated signing. + * Throws if init() has not been called and awaited. + */ +export function createExchange( + exchangeId: T, + config: Record = {}, +): InstanceType<(typeof ccxt)[T]> { + if (!_initPromise) { + throw new Error("@octobot/edge: init() must be called before createExchange()") + } + if (!_initResolved) { + throw new Error("@octobot/edge: init() must be awaited before calling createExchange()") + } + const Cls = ccxt[exchangeId] as new (cfg: Record) => InstanceType<(typeof ccxt)[T]> + const exchange = new Cls(config) + patchExchange(exchange as any, _bridge) + return exchange +} + +/** Re-export ccxt for consumers */ +export { ccxt } +export type { Runtime } from "./polyfills/detect" +export type { RustBridge } from "./rust-bridge/types" diff --git a/packages/edge/js/src/polyfills/browser.ts b/packages/edge/js/src/polyfills/browser.ts new file mode 100644 index 0000000000..94d131f995 --- /dev/null +++ b/packages/edge/js/src/polyfills/browser.ts @@ -0,0 +1,158 @@ +/** + * Browser polyfills — patches globalThis with: + * - crypto.createHmac / createHash / randomBytes + * - Buffer (BrowserBuffer backed by Rust ops via RustBridge) + * + * Call installBrowserPolyfills() once before importing ccxt. + */ +import type { RustBridge } from "../rust-bridge/types" + +interface HashBuilder { + update(data: string | Uint8Array): HashBuilder + digest(encoding: string): string +} + +let installed = false + +export async function installBrowserPolyfills(bridge: RustBridge): Promise { + if (installed) return + installed = true + + const g = globalThis as any + + // ── crypto ───────────────────────────────────────────────────────────── + + const enc = new TextEncoder() + + function concatChunks(chunks: Uint8Array[]): Uint8Array { + const total = chunks.reduce((s, c) => s + c.length, 0) + const result = new Uint8Array(total) + let offset = 0 + for (const c of chunks) { result.set(c, offset); offset += c.length } + return result + } + + function makeHmacBuilder(algorithm: string, secret: string): HashBuilder { + const chunks: Uint8Array[] = [] + const builder: HashBuilder = { + update(data: string | Uint8Array): HashBuilder { + chunks.push(typeof data === "string" ? enc.encode(data) : data) + return builder + }, + digest(encoding: string): string { + const message = concatChunks(chunks) + const secretBytes = enc.encode(secret) + const algo = algorithm.toLowerCase() + // Use byte-level bridge methods to avoid lossy string roundtripping + if (algo === "sha256" && encoding === "hex") return bridge.hmacSha256Hex(secretBytes, message) + if (algo === "sha512" && encoding === "hex") return bridge.hmacSha512Hex(secretBytes, message) + // Fallback to string-based bridge for other algorithms/encodings + const accumulated = new TextDecoder().decode(message) + return bridge.hmac(algorithm, secret, accumulated, encoding) + }, + } + return builder + } + + function makeHashBuilder(algorithm: string): HashBuilder { + let accumulated = "" + const builder: HashBuilder = { + update(data: string | Uint8Array): HashBuilder { + accumulated += typeof data === "string" + ? data + : new TextDecoder().decode(data) + return builder + }, + digest(encoding: string): string { + const result = bridge.hash(algorithm, accumulated, encoding) + accumulated = "" + return result + }, + } + return builder + } + + const cryptoObj = (g.crypto ?? {}) as any + const originalGetRandomValues = typeof cryptoObj.getRandomValues === "function" + ? cryptoObj.getRandomValues.bind(cryptoObj) + : undefined + + const defineCryptoMethod = (name: string, value: unknown) => { + try { + Object.defineProperty(cryptoObj, name, { + value, + configurable: true, + enumerable: false, + writable: true, + }) + } catch { + cryptoObj[name] = value + } + } + + defineCryptoMethod("createHmac", (algorithm: string, secret: string) => + makeHmacBuilder(algorithm, secret), + ) + defineCryptoMethod("createHash", (algorithm: string) => + makeHashBuilder(algorithm), + ) + defineCryptoMethod("randomBytes", (size: number) => { + const bytes = bridge.randomBytes(size) + return new BrowserBuffer(bytes) + }) + if (originalGetRandomValues) { + defineCryptoMethod("getRandomValues", originalGetRandomValues) + } + g.crypto = cryptoObj + + // ── Buffer ───────────────────────────────────────────────────────────── + + class BrowserBuffer extends Uint8Array { + // Use a static method name that doesn't conflict with Uint8Array.from overloads + static fromValue( + value: string | ArrayLike | ArrayBufferLike, + encoding?: string, + ): BrowserBuffer { + if (typeof value === "string") { + const enc = encoding ?? "utf8" + let bytes: Uint8Array + if (enc === "hex") bytes = new Uint8Array(bridge.bufferFromHex(value)) + else if (enc === "base64") bytes = new Uint8Array(bridge.bufferFromBase64(value)) + else if (enc === "binary" || enc === "latin1") { + bytes = new Uint8Array(value.length) + for (let i = 0; i < value.length; i++) bytes[i] = value.charCodeAt(i) & 0xff + } else bytes = new TextEncoder().encode(value) + // Use bytes directly (not bytes.buffer) to avoid aliasing WASM linear memory + return new BrowserBuffer(bytes) + } + if (value instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer)) { + return new BrowserBuffer(new Uint8Array(value as ArrayBuffer)) + } + return new BrowserBuffer(value as ArrayLike) + } + static alloc(size: number) { return new BrowserBuffer(size) } + static concat(bufs: BrowserBuffer[]): BrowserBuffer { + const total = bufs.reduce((s, b) => s + b.length, 0) + const result = new BrowserBuffer(total) + let offset = 0 + for (const b of bufs) { result.set(b, offset); offset += b.length } + return result + } + static byteLength(str: string) { return bridge.bufferByteLength(str) } + static isBuffer(obj: unknown) { return obj instanceof BrowserBuffer } + override toString(encoding?: string): string { + if (encoding === "hex") return bridge.bufferToHex(this) + if (encoding === "base64") return bridge.bufferToBase64(this) + if (encoding === "binary" || encoding === "latin1") { + let result = "" + for (let i = 0; i < this.length; i++) result += String.fromCharCode(this[i]) + return result + } + return new TextDecoder().decode(this) + } + } + + // Alias .from to .fromValue — avoids TypeScript override conflict with Uint8Array.from + ;(BrowserBuffer as any).from = BrowserBuffer.fromValue + g.Buffer = BrowserBuffer +} diff --git a/packages/edge/js/src/polyfills/crypto.ts b/packages/edge/js/src/polyfills/crypto.ts new file mode 100644 index 0000000000..3e17de686a --- /dev/null +++ b/packages/edge/js/src/polyfills/crypto.ts @@ -0,0 +1,72 @@ +/** + * crypto.subtle shim that delegates to Rust when available. + * Used in environments without native Web Crypto API. + */ +import type { RustBridge } from "../rust-bridge/types" + +interface HashBuilder { + update(data: string | Uint8Array): HashBuilder + digest(encoding: string): string +} + +export function installCryptoPolyfill(bridge: RustBridge): void { + const g = globalThis as any + + if (!g.crypto) { + g.crypto = {} + } + + const enc = new TextEncoder() + + function concatChunks(chunks: Uint8Array[]): Uint8Array { + const total = chunks.reduce((s, c) => s + c.length, 0) + const result = new Uint8Array(total) + let offset = 0 + for (const c of chunks) { result.set(c, offset); offset += c.length } + return result + } + + g.crypto.createHmac = (algorithm: string, secret: string): HashBuilder => { + const chunks: Uint8Array[] = [] + const builder: HashBuilder = { + update(data: string | Uint8Array): HashBuilder { + chunks.push(typeof data === "string" ? enc.encode(data) : data) + return builder + }, + digest(encoding: string): string { + const message = concatChunks(chunks) + const secretBytes = enc.encode(secret) + const algo = algorithm.toLowerCase() + if (algo === "sha256" && encoding === "hex") return bridge.hmacSha256Hex(secretBytes, message) + if (algo === "sha512" && encoding === "hex") return bridge.hmacSha512Hex(secretBytes, message) + const accumulated = new TextDecoder().decode(message) + return bridge.hmac(algorithm, secret, accumulated, encoding) + }, + } + return builder + } + + g.crypto.createHash = (algorithm: string): HashBuilder => { + let accumulated = "" + const builder: HashBuilder = { + update(data: string | Uint8Array): HashBuilder { + accumulated += typeof data === "string" + ? data + : new TextDecoder().decode(data) + return builder + }, + digest(encoding: string): string { + const result = bridge.hash(algorithm, accumulated, encoding) + accumulated = "" + return result + }, + } + return builder + } + + if (typeof g.crypto.randomBytes !== "function") { + g.crypto.randomBytes = (size: number): Uint8Array => { + return bridge.randomBytes(size) + } + } +} diff --git a/packages/edge/js/src/polyfills/detect.ts b/packages/edge/js/src/polyfills/detect.ts new file mode 100644 index 0000000000..f071ea4768 --- /dev/null +++ b/packages/edge/js/src/polyfills/detect.ts @@ -0,0 +1,12 @@ +export type Runtime = "node" | "bun" | "deno" | "browser" | "react-native" + +export function detectRuntime(): Runtime { + const g = globalThis as any + if (typeof g.Deno !== "undefined") return "deno" + if (typeof g.Bun !== "undefined") return "bun" + if (typeof navigator !== "undefined" && (navigator as any).product === "ReactNative") return "react-native" + // Check for browser window or Web Worker / Service Worker environment + if (typeof window !== "undefined") return "browser" + if (typeof g.self === "object" && typeof g.WorkerGlobalScope !== "undefined") return "browser" + return "node" +} diff --git a/packages/edge/js/src/polyfills/encoding.ts b/packages/edge/js/src/polyfills/encoding.ts new file mode 100644 index 0000000000..79d0265a06 --- /dev/null +++ b/packages/edge/js/src/polyfills/encoding.ts @@ -0,0 +1,98 @@ +/** + * TextEncoder/Decoder polyfill for Hermes (React Native). + * Only installed if not already available. + */ +export function installEncodingPolyfill(): void { + if (typeof globalThis.TextEncoder === "undefined") { + (globalThis as any).TextEncoder = class TextEncoder { + encode(input: string): Uint8Array { + // Proper UTF-8 encoding: handle multi-byte characters and surrogate pairs + const bytes: number[] = [] + for (let i = 0; i < input.length; i++) { + let cp = input.codePointAt(i)! + if (cp >= 0xd800 && cp <= 0xdfff) { + // Lone surrogate — encode as U+FFFD replacement character (3-byte UTF-8) + cp = 0xfffd + } + if (cp < 0x80) { + bytes.push(cp) + } else if (cp < 0x800) { + bytes.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f)) + } else if (cp < 0x10000) { + bytes.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)) + } else { + bytes.push( + 0xf0 | (cp >> 18), + 0x80 | ((cp >> 12) & 0x3f), + 0x80 | ((cp >> 6) & 0x3f), + 0x80 | (cp & 0x3f), + ) + i++ // skip low surrogate of the pair + } + } + return new Uint8Array(bytes) + } + } + } + + if (typeof globalThis.TextDecoder === "undefined") { + (globalThis as any).TextDecoder = class TextDecoder { + decode(input: Uint8Array): string { + // Proper UTF-8 decoding: reconstruct multi-byte sequences + let result = "" + let i = 0 + while (i < input.length) { + const b0 = input[i] + if (b0 < 0x80) { + // 1-byte (ASCII) + result += String.fromCodePoint(b0) + i += 1 + } else if ((b0 & 0xe0) === 0xc0) { + // 2-byte sequence + if (i + 1 >= input.length) { result += "\ufffd"; i += 1; continue } + const b1 = input[i + 1] + if ((b1 & 0xc0) !== 0x80) { result += "\ufffd"; i += 1; continue } + const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f) + // Reject overlong encoding + result += cp >= 0x80 ? String.fromCodePoint(cp) : "\ufffd" + i += 2 + } else if ((b0 & 0xf0) === 0xe0) { + // 3-byte sequence + if (i + 2 >= input.length) { result += "\ufffd"; i += 1; continue } + const b1 = input[i + 1] + const b2 = input[i + 2] + if ((b1 & 0xc0) !== 0x80 || (b2 & 0xc0) !== 0x80) { result += "\ufffd"; i += 1; continue } + const cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f) + // Reject overlong and surrogate range + if (cp < 0x800 || (cp >= 0xd800 && cp <= 0xdfff)) { + result += "\ufffd" + } else { + result += String.fromCodePoint(cp) + } + i += 3 + } else if ((b0 & 0xf8) === 0xf0) { + // 4-byte sequence + if (i + 3 >= input.length) { result += "\ufffd"; i += 1; continue } + const b1 = input[i + 1] + const b2 = input[i + 2] + const b3 = input[i + 3] + if ((b1 & 0xc0) !== 0x80 || (b2 & 0xc0) !== 0x80 || (b3 & 0xc0) !== 0x80) { result += "\ufffd"; i += 1; continue } + const cp = ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f) + // Reject overlong and out-of-range + if (cp < 0x10000 || cp > 0x10ffff) { + result += "\ufffd" + } else { + result += String.fromCodePoint(cp) + } + i += 4 + } else { + // Invalid lead byte + result += "\ufffd" + i += 1 + } + } + return result + } + } + } +} diff --git a/packages/edge/js/src/polyfills/mobile.ts b/packages/edge/js/src/polyfills/mobile.ts new file mode 100644 index 0000000000..e4dcd8f758 --- /dev/null +++ b/packages/edge/js/src/polyfills/mobile.ts @@ -0,0 +1,296 @@ +/** + * Mobile polyfills (iOS + Android via UniFFI Turbo Module). + * + * Patches globalThis before ccxt loads with: + * - crypto (createHmac, createHash, randomBytes) + * - fetch (Rust reqwest fallback if native unavailable) + * - Buffer (RustBackedBuffer) + * - URLSearchParams, btoa/atob, process, performance, DOMException + * + * Import this module BEFORE loading ccxt: + * import '@octobot/edge/polyfills/mobile' + */ + +// These imports are resolved by uniffi-bindgen-react-native at build time. +// In development/testing, this module is not imported directly. +import { + hmac, + hash, + randomBytesHex, + randomBytes, + bufferToHex, + bufferFromHex, + bufferToBase64, + bufferFromBase64, + bufferByteLength, + fetch as rustFetch, + type FetchRequest, + type FetchResponse, +} from "../generated/octobot_edge_mobile" + +// ── 1. crypto ─────────────────────────────────────────────────────────────── +// +// CCXT calls: crypto.createHmac(algo, key).update(data).digest(enc) + +interface HashBuilder { + update(data: string | Uint8Array): HashBuilder + digest(encoding: string): string +} + +function makeHmacBuilder(algorithm: string, secret: string): HashBuilder { + // Accumulate as string since the mobile UniFFI bridge only exposes string-based hmac(). + // In the mobile/ccxt signing path, data is always ASCII query strings, so this is safe. + let accumulated = "" + const builder: HashBuilder = { + update(data: string | Uint8Array): HashBuilder { + accumulated += typeof data === "string" + ? data + : new TextDecoder().decode(data) + return builder + }, + digest(encoding: string): string { + const result = hmac(algorithm, secret, accumulated, encoding) + accumulated = "" + return result + }, + } + return builder +} + +function makeHashBuilder(algorithm: string): HashBuilder { + let accumulated = "" + const builder: HashBuilder = { + update(data: string | Uint8Array): HashBuilder { + accumulated += typeof data === "string" + ? data + : new TextDecoder().decode(data) + return builder + }, + digest(encoding: string): string { + const result = hash(algorithm, accumulated, encoding) + accumulated = "" + return result + }, + } + return builder +} + +// Mutate the existing crypto object rather than spreading it, to preserve +// non-enumerable properties, getters (e.g. subtle), and the prototype chain. +const cryptoObj: any = (globalThis as any).crypto ?? {} + +cryptoObj.createHmac = (algorithm: string, secret: string) => + makeHmacBuilder(algorithm, secret) + +cryptoObj.createHash = (algorithm: string) => + makeHashBuilder(algorithm) + +cryptoObj.randomBytes = (size: number): InstanceType => { + const bytes = randomBytes(size) + return RustBackedBuffer.from(bytes) +} + +;(globalThis as any).crypto = cryptoObj + +// ── 2. fetch ──────────────────────────────────────────────────────────────── +// +// Prefer React Native's native fetch; keep Rust fetch as fallback. + +const nativeFetch = (globalThis as any).fetch?.bind(globalThis) + +if (typeof nativeFetch !== "function") { + ;(globalThis as any).fetch = async ( + url: string, + options: { + method?: string + headers?: Record + body?: string + } = {}, + ): Promise<{ + status: number + ok: boolean + headers: { get: (name: string) => string | null } + text: () => Promise + json: () => Promise + }> => { + const req: FetchRequest = { + url, + method: options.method ?? "GET", + headers: Object.entries(options.headers ?? {}).map(([name, value]) => ({ + name, + value, + })), + body: options.body ?? null, + } + + const res: FetchResponse = await rustFetch(req) + const headerMap = new Map(res.headers.map((h: any) => [h.name.toLowerCase(), h.value])) + + return { + status: res.status, + ok: res.ok, + headers: { + get: (name: string) => headerMap.get(name.toLowerCase()) ?? null, + }, + text: async () => res.body, + json: async () => JSON.parse(res.body), + } + } +} + +// ── 3. Buffer ─────────────────────────────────────────────────────────────── +// +// CCXT uses Buffer for: from(str, enc), toString(enc), concat, byteLength. + +class RustBackedBuffer extends Uint8Array { + // Use a static method name that doesn't conflict with Uint8Array.from overloads + static fromValue( + value: string | ArrayLike | ArrayBufferLike, + encoding?: string, + ): RustBackedBuffer { + if (typeof value === "string") { + const enc = encoding ?? "utf8" + let bytes: Uint8Array + if (enc === "hex") { + bytes = new Uint8Array(bufferFromHex(value)) + } else if (enc === "base64") { + bytes = new Uint8Array(bufferFromBase64(value)) + } else if (enc === "binary" || enc === "latin1") { + // Latin-1: each char code (0-255) maps to one byte + bytes = new Uint8Array(value.length) + for (let i = 0; i < value.length; i++) { + bytes[i] = value.charCodeAt(i) & 0xff + } + } else { + bytes = new TextEncoder().encode(value) + } + return new RustBackedBuffer(bytes.buffer as ArrayBuffer) + } + if (value instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer)) { + return new RustBackedBuffer(new Uint8Array(value as ArrayBuffer)) + } + return new RustBackedBuffer(value as ArrayLike) + } + + static alloc(size: number): RustBackedBuffer { + return new RustBackedBuffer(size) + } + + static concat(bufs: RustBackedBuffer[]): RustBackedBuffer { + const total = bufs.reduce((s, b) => s + b.length, 0) + const result = new RustBackedBuffer(total) + let offset = 0 + for (const b of bufs) { + result.set(b, offset) + offset += b.length + } + return result + } + + static byteLength(str: string): number { + return bufferByteLength(str) + } + + static isBuffer(obj: unknown): boolean { + return obj instanceof RustBackedBuffer + } + + override toString(encoding?: string): string { + const enc = encoding ?? "utf8" + if (enc === "hex") return bufferToHex(this) + if (enc === "base64") return bufferToBase64(this) + if (enc === "binary" || enc === "latin1") { + let result = "" + for (let i = 0; i < this.length; i++) { + result += String.fromCharCode(this[i]) + } + return result + } + return new TextDecoder("utf-8").decode(this) + } +} + +// Alias .from to .fromValue — avoids TypeScript override conflict with Uint8Array.from +;(RustBackedBuffer as any).from = RustBackedBuffer.fromValue +;(globalThis as any).Buffer = RustBackedBuffer + +// ── 4. Remaining pure-JS polyfills ────────────────────────────────────────── + +if (typeof (globalThis as any).URLSearchParams === "undefined") { + ;(globalThis as any).URLSearchParams = class URLSearchParams { + private _params: [string, string][] = [] + constructor(init?: Record | string) { + if (typeof init === "string") { + init + .replace(/^\?/, "") + .split("&") + .forEach((pair) => { + const idx = pair.indexOf("=") + const k = idx >= 0 ? pair.slice(0, idx) : pair + const v = idx >= 0 ? pair.slice(idx + 1) : "" + if (k) this._params.push([decodeURIComponent(k), decodeURIComponent(v)]) + }) + } else if (init) { + Object.entries(init).forEach(([k, v]) => this._params.push([k, v])) + } + } + append(k: string, v: string) { this._params.push([k, String(v)]) } + get(k: string) { return this._params.find((p) => p[0] === k)?.[1] ?? null } + getAll(k: string) { return this._params.filter((p) => p[0] === k).map((p) => p[1]) } + has(k: string) { return this._params.some((p) => p[0] === k) } + set(k: string, v: string) { + this._params = this._params.filter((p) => p[0] !== k) + this._params.push([k, String(v)]) + } + delete(k: string) { this._params = this._params.filter((p) => p[0] !== k) } + *keys() { for (const [k] of this._params) yield k } + *values() { for (const [, v] of this._params) yield v } + *entries() { for (const pair of this._params) yield pair as [string, string] } + forEach(cb: (value: string, key: string, parent: URLSearchParams) => void) { + for (const [k, v] of this._params) cb(v, k, this) + } + [Symbol.iterator]() { return this.entries() } + toString() { + return this._params + .map((p) => `${encodeURIComponent(p[0])}=${encodeURIComponent(p[1])}`) + .join("&") + } + } +} + +if (typeof (globalThis as any).btoa === "undefined") { + // btoa/atob use Latin-1 (each char code 0-255 maps to one byte) + ;(globalThis as any).btoa = (str: string): string => { + const bytes = new Uint8Array(str.length) + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i) + if (code > 255) throw new Error("btoa: character out of Latin-1 range") + bytes[i] = code + } + return bufferToBase64(bytes) + } + ;(globalThis as any).atob = (b64: string): string => { + const bytes = new Uint8Array(bufferFromBase64(b64)) + let result = "" + for (let i = 0; i < bytes.length; i++) { + result += String.fromCharCode(bytes[i]) + } + return result + } +} + +;(globalThis as any).process = (globalThis as any).process ?? { env: Object.create(null), version: "v18.0.0" } +;(globalThis as any).performance = (globalThis as any).performance ?? { now: () => Date.now() } + +if (typeof (globalThis as any).DOMException === "undefined") { + class RNPolyfillDOMException extends Error { + name: string + constructor(message = "", name = "Error") { + super(message) + this.name = name + } + } + ;(globalThis as any).DOMException = RNPolyfillDOMException +} + +export {} diff --git a/packages/edge/js/src/polyfills/node.ts b/packages/edge/js/src/polyfills/node.ts new file mode 100644 index 0000000000..339f3401e3 --- /dev/null +++ b/packages/edge/js/src/polyfills/node.ts @@ -0,0 +1,46 @@ +/** + * Node.js polyfills for testing. + * Patches globalThis to use Node.js built-in crypto and Buffer. + */ + +import * as nodeCrypto from 'crypto'; + +if (!globalThis.crypto) { + (globalThis as any).crypto = {}; +} + +(globalThis.crypto as any).createHmac = (algorithm: string, key: string | Buffer) => { + const hmac = nodeCrypto.createHmac(algorithm, key); + const builder = { + update: (data: string | Buffer) => { + hmac.update(data); + return builder; + }, + digest: (encoding: string) => { + return hmac.digest(encoding as any); + }, + }; + return builder; +}; + +(globalThis.crypto as any).createHash = (algorithm: string) => { + const hash = nodeCrypto.createHash(algorithm); + const builder = { + update: (data: string | Buffer) => { + hash.update(data); + return builder; + }, + digest: (encoding: string) => { + return hash.digest(encoding as any); + }, + }; + return builder; +}; + +(globalThis.crypto as any).randomBytes = nodeCrypto.randomBytes; + +if (!globalThis.Buffer) { + (globalThis as any).Buffer = Buffer; +} + +export {}; diff --git a/packages/edge/js/src/rust-bridge/native.ts b/packages/edge/js/src/rust-bridge/native.ts new file mode 100644 index 0000000000..cb5067c55c --- /dev/null +++ b/packages/edge/js/src/rust-bridge/native.ts @@ -0,0 +1,34 @@ +import type { RustBridge } from "./types" + +let bridge: RustBridge | null = null + +// napi-rs auto-converts snake_case to camelCase, so these match RustBridge. +const REQUIRED_METHODS: (keyof RustBridge)[] = [ + "hmac", "hash", "hmacSha256", "hmacSha256Hex", "hmacSha512", "hmacSha512Hex", + "randomBytesHex", "randomBytes", + "bufferToHex", "bufferFromHex", "bufferToBase64", "bufferFromBase64", "bufferByteLength", +] + +export async function loadNative(): Promise { + if (bridge) return bridge + try { + // napi-rs generates this via `napi build --js` + const mod = await import("./native.node") + + // napi-rs converts snake_case Rust names to camelCase JS names. + // Validate all required methods exist. + for (const method of REQUIRED_METHODS) { + if (typeof mod[method] !== "function") { + // If camelCase not found, the NAPI build may have used different naming. + // Log which method is missing for debugging. + console.warn(`[edge] Native bridge missing method: ${method}`) + return null + } + } + bridge = mod as RustBridge + return bridge + } catch { + // Native module not available (expected in browser/WASM environments) + return null + } +} diff --git a/packages/edge/js/src/rust-bridge/types.ts b/packages/edge/js/src/rust-bridge/types.ts new file mode 100644 index 0000000000..3fb903d283 --- /dev/null +++ b/packages/edge/js/src/rust-bridge/types.ts @@ -0,0 +1,20 @@ +export interface RustBridge { + // HMAC + hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array + hmacSha256Hex(key: Uint8Array, message: Uint8Array): string + hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array + hmacSha512Hex(key: Uint8Array, message: Uint8Array): string + hmac(algorithm: string, secret: string, data: string, encoding: string): string + hash(algorithm: string, data: string, encoding: string): string + + // Random + randomBytesHex(size: number): string + randomBytes(size: number): Uint8Array + + // Buffer operations + bufferToHex(data: Uint8Array): string + bufferFromHex(hexStr: string): Uint8Array + bufferToBase64(data: Uint8Array): string + bufferFromBase64(b64: string): Uint8Array + bufferByteLength(utf8Str: string): number +} diff --git a/packages/edge/js/src/rust-bridge/wasm.ts b/packages/edge/js/src/rust-bridge/wasm.ts new file mode 100644 index 0000000000..f3ef911a6e --- /dev/null +++ b/packages/edge/js/src/rust-bridge/wasm.ts @@ -0,0 +1,48 @@ +import type { RustBridge } from "./types" + +let bridge: RustBridge | null = null + +const WASM_REQUIRED_EXPORTS = [ + "hmac", "hash", "hmac_sha256", "hmac_sha256_hex", + "hmac_sha512", "hmac_sha512_hex", + "random_bytes_hex", "random_bytes", + "buffer_to_hex", "buffer_from_hex", "buffer_to_base64", "buffer_from_base64", "buffer_byte_length", +] as const + +export async function loadWasm(): Promise { + if (bridge) return bridge + try { + // Generated by wasm-pack + const mod = await import("./pkg/octobot_edge_wasm.js") + await mod.default() + + // Validate all required exports exist + for (const fn of WASM_REQUIRED_EXPORTS) { + if (typeof mod[fn] !== "function") { + console.warn(`[edge] WASM module missing export: ${fn}`) + return null + } + } + + // wasm-pack exports use snake_case; map to camelCase RustBridge interface + bridge = { + hmac: mod.hmac, + hash: mod.hash, + hmacSha256: mod.hmac_sha256, + hmacSha256Hex: mod.hmac_sha256_hex, + hmacSha512: mod.hmac_sha512, + hmacSha512Hex: mod.hmac_sha512_hex, + randomBytesHex: mod.random_bytes_hex, + randomBytes: mod.random_bytes, + bufferToHex: mod.buffer_to_hex, + bufferFromHex: mod.buffer_from_hex, + bufferToBase64: mod.buffer_to_base64, + bufferFromBase64: mod.buffer_from_base64, + bufferByteLength: mod.buffer_byte_length, + } + return bridge + } catch (err) { + console.warn("[edge] Failed to load WASM bridge:", err) + return null + } +} diff --git a/packages/edge/js/tests/binance.integration.test.ts b/packages/edge/js/tests/binance.integration.test.ts new file mode 100644 index 0000000000..90fa7703cd --- /dev/null +++ b/packages/edge/js/tests/binance.integration.test.ts @@ -0,0 +1,218 @@ +/** + * Binance API integration tests using real ccxt exchange with polyfills. + * + * Tests the full polyfill stack: crypto (HMAC, hash), Buffer, and + * ccxt exchange operations against the live Binance public API. + * + * Run with: npm test -- tests/binance.integration.test.ts + */ +import { describe, test, expect, beforeAll } from "@jest/globals" +import { createHmac, createHash } from "crypto" + +let ccxt: any +const g = globalThis as any + +describe("CCXT Binance Integration", () => { + beforeAll(async () => { + // Load Node.js polyfills for testing + await import("../src/polyfills/node") + ccxt = (await import("ccxt")).default + }) + + // ── Polyfill availability ────────────────────────────────────────────── + + test("polyfills are loaded", () => { + expect(typeof g.crypto).toBe("object") + expect(typeof g.crypto.createHmac).toBe("function") + expect(typeof g.crypto.createHash).toBe("function") + expect(typeof g.Buffer).toBe("function") + }) + + test("Buffer encoding/decoding roundtrip", () => { + const original = "Hello, CCXT!" + + const buf = g.Buffer.from(original, "utf8") + expect(buf.toString("utf8")).toBe(original) + + const hex = buf.toString("hex") + expect(g.Buffer.from(hex, "hex").toString("utf8")).toBe(original) + + const b64 = buf.toString("base64") + expect(g.Buffer.from(b64, "base64").toString("utf8")).toBe(original) + }) + + // ── Crypto functions ─────────────────────────────────────────────────── + + describe("Crypto Functions", () => { + test("HMAC-SHA256 Binance signature vector", () => { + const secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + const message = + "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" + + const result = g.crypto.createHmac("sha256", secret).update(message).digest("hex") + expect(result).toBe("c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71") + }) + + test("SHA-256 hash", () => { + const result = g.crypto.createHash("sha256").update("hello world").digest("hex") + expect(result).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + }) + + test("MD5 hash", () => { + const result = g.crypto.createHash("md5").update("hello world").digest("hex") + expect(result).toBe("5eb63bbbe01eeed093cb22bb8f5acdc3") + }) + + test("HMAC chained updates produce correct result", () => { + const polyfill = g.crypto.createHmac("sha256", "secret") + .update("part1") + .update("part2") + .digest("hex") + + const native = createHmac("sha256", "secret") + .update("part1") + .update("part2") + .digest("hex") + + expect(polyfill).toBe(native) + }) + + test("hash chained updates produce correct result", () => { + const polyfill = g.crypto.createHash("sha256") + .update("a") + .update("b") + .update("c") + .digest("hex") + + const native = createHash("sha256") + .update("a") + .update("b") + .update("c") + .digest("hex") + + expect(polyfill).toBe(native) + }) + }) + + // ── Binance Public API ───────────────────────────────────────────────── + + describe("Binance Public API", () => { + let exchange: any + + beforeAll(() => { + exchange = new ccxt.binance({ + enableRateLimit: true, + timeout: 30000, + }) + }) + + test("fetch server time", async () => { + const time = await exchange.fetchTime() + + expect(typeof time).toBe("number") + expect(time).toBeGreaterThan(1577836800000) // After 2020-01-01 + expect(time).toBeLessThan(Date.now() + 60000) // Not too far in future + }, 30000) + + test("fetch BTC/USDT ticker", async () => { + const ticker = await exchange.fetchTicker("BTC/USDT") + + expect(ticker).toBeDefined() + expect(ticker.symbol).toBe("BTC/USDT") + expect(typeof ticker.last).toBe("number") + expect(ticker.last).toBeGreaterThan(0) + expect(typeof ticker.bid).toBe("number") + expect(typeof ticker.ask).toBe("number") + expect(ticker.ask).toBeGreaterThan(ticker.bid) + }, 30000) + + test("fetch ETH/USDT ticker", async () => { + const ticker = await exchange.fetchTicker("ETH/USDT") + + expect(ticker.symbol).toBe("ETH/USDT") + expect(ticker.last).toBeGreaterThan(0) + }, 30000) + + test("fetch order book", async () => { + const orderBook = await exchange.fetchOrderBook("BTC/USDT", 10) + + expect(orderBook).toBeDefined() + expect(Array.isArray(orderBook.bids)).toBe(true) + expect(Array.isArray(orderBook.asks)).toBe(true) + expect(orderBook.bids.length).toBeGreaterThan(0) + expect(orderBook.asks.length).toBeGreaterThan(0) + + // Bids should be in descending order + expect(orderBook.bids[0][0]).toBeGreaterThan(orderBook.bids[1][0]) + // Asks should be in ascending order + expect(orderBook.asks[0][0]).toBeLessThan(orderBook.asks[1][0]) + }, 30000) + + test("fetch recent trades", async () => { + const trades = await exchange.fetchTrades("BTC/USDT", undefined, 10) + + expect(Array.isArray(trades)).toBe(true) + expect(trades.length).toBeGreaterThan(0) + expect(trades.length).toBeLessThanOrEqual(10) + + const trade = trades[0] + expect(trade.id).toBeDefined() + expect(typeof trade.timestamp).toBe("number") + expect(typeof trade.price).toBe("number") + expect(typeof trade.amount).toBe("number") + expect(["buy", "sell"]).toContain(trade.side) + }, 30000) + + test("fetch OHLCV (candlestick data)", async () => { + const ohlcv = await exchange.fetchOHLCV("BTC/USDT", "1h", undefined, 5) + + expect(Array.isArray(ohlcv)).toBe(true) + expect(ohlcv.length).toBeGreaterThan(0) + expect(ohlcv.length).toBeLessThanOrEqual(5) + + const candle = ohlcv[0] + expect(candle.length).toBe(6) // [timestamp, open, high, low, close, volume] + for (let i = 0; i < 6; i++) { + expect(typeof candle[i]).toBe("number") + } + }, 30000) + + test("fetch markets", async () => { + const markets = await exchange.fetchMarkets() + + expect(Array.isArray(markets)).toBe(true) + expect(markets.length).toBeGreaterThan(100) + + const btcUsdt = markets.find((m: any) => m.symbol === "BTC/USDT") + expect(btcUsdt).toBeDefined() + expect(btcUsdt.base).toBe("BTC") + expect(btcUsdt.quote).toBe("USDT") + expect(btcUsdt.active).toBe(true) + }, 30000) + }) + + // ── Multiple Exchanges ───────────────────────────────────────────────── + + describe("Multiple Exchanges", () => { + test("fetch prices from multiple exchanges", async () => { + const exchanges = [ + { name: "Binance", instance: new ccxt.binance() }, + { name: "Binance US", instance: new ccxt.binanceus() }, + ] + + const results: { exchange: string; success: boolean }[] = [] + + for (const { name, instance } of exchanges) { + try { + const ticker = await instance.fetchTicker("BTC/USDT") + results.push({ exchange: name, success: true }) + } catch { + results.push({ exchange: name, success: false }) + } + } + + // At least one should succeed + expect(results.some((r) => r.success)).toBe(true) + }, 60000) + }) +}) diff --git a/packages/edge/js/tests/browser.test.ts b/packages/edge/js/tests/browser.test.ts new file mode 100644 index 0000000000..cda3db86ca --- /dev/null +++ b/packages/edge/js/tests/browser.test.ts @@ -0,0 +1,131 @@ +/** + * Tests for the browser polyfill (installBrowserPolyfills). + * Uses a mock RustBridge backed by Node.js crypto. + */ +import { describe, test, expect, beforeAll, afterAll } from "@jest/globals" +import { installBrowserPolyfills } from "../src/polyfills/browser" +import type { RustBridge } from "../src/rust-bridge/types" +import { createHmac, createHash, randomBytes as nodeRandomBytes } from "crypto" + +// Capture the native Node Buffer BEFORE polyfills overwrite globalThis.Buffer +const NodeBuffer = Buffer + +function createNodeBridge(): RustBridge { + return { + hmacSha256: (key, message) => new Uint8Array(createHmac("sha256", key).update(message).digest()), + hmacSha256Hex: (key, message) => createHmac("sha256", key).update(message).digest("hex"), + hmacSha512: (key, message) => new Uint8Array(createHmac("sha512", key).update(message).digest()), + hmacSha512Hex: (key, message) => createHmac("sha512", key).update(message).digest("hex"), + hmac: (algorithm, secret, data, encoding) => createHmac(algorithm, secret).update(data).digest(encoding as any), + hash: (algorithm, data, encoding) => createHash(algorithm).update(data).digest(encoding as any), + randomBytesHex: (size) => nodeRandomBytes(size).toString("hex"), + randomBytes: (size) => new Uint8Array(nodeRandomBytes(size)), + bufferToHex: (data) => NodeBuffer.from(data).toString("hex"), + bufferFromHex: (hexStr) => new Uint8Array(NodeBuffer.from(hexStr, "hex")), + bufferToBase64: (data) => NodeBuffer.from(data).toString("base64"), + bufferFromBase64: (b64) => new Uint8Array(NodeBuffer.from(b64, "base64")), + bufferByteLength: (utf8Str) => NodeBuffer.byteLength(utf8Str, "utf8"), + } +} + +describe("installBrowserPolyfills", () => { + const g = globalThis as any + let savedCrypto: any + let savedBuffer: any + + beforeAll(async () => { + savedCrypto = g.crypto + savedBuffer = g.Buffer + // Reset to simulate browser + g.crypto = { getRandomValues: () => {} } + delete g.Buffer + + const bridge = createNodeBridge() + await installBrowserPolyfills(bridge) + }) + + afterAll(() => { + g.crypto = savedCrypto + g.Buffer = savedBuffer + }) + + // ── Crypto polyfills ───────────────────────────────────────────────── + + test("installs crypto.createHmac", () => { + expect(typeof g.crypto.createHmac).toBe("function") + const result = g.crypto.createHmac("sha256", "secret").update("message").digest("hex") + expect(result.length).toBe(64) + }) + + test("installs crypto.createHash", () => { + expect(typeof g.crypto.createHash).toBe("function") + const result = g.crypto.createHash("sha256").update("hello world").digest("hex") + expect(result).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + }) + + test("installs crypto.randomBytes", () => { + expect(typeof g.crypto.randomBytes).toBe("function") + const result = g.crypto.randomBytes(16) + expect(result).toBeInstanceOf(Uint8Array) + expect(result.length).toBe(16) + }) + + test("preserves existing getRandomValues", () => { + // getRandomValues should be preserved from the original crypto + expect(typeof g.crypto.getRandomValues).toBe("function") + }) + + // ── Buffer polyfill ────────────────────────────────────────────────── + + test("installs globalThis.Buffer", () => { + expect(typeof g.Buffer).toBe("function") + }) + + test("Buffer.from(string, utf8) roundtrips", () => { + const buf = g.Buffer.from("hello", "utf8") + expect(buf.toString()).toBe("hello") + }) + + test("Buffer.from(string, hex) roundtrips", () => { + const buf = g.Buffer.from("deadbeef", "hex") + expect(buf.toString("hex")).toBe("deadbeef") + }) + + test("Buffer.from(string, base64) roundtrips", () => { + const buf = g.Buffer.from("aGVsbG8=", "base64") + expect(buf.toString("base64")).toBe("aGVsbG8=") + }) + + test("Buffer.alloc creates zero-filled buffer", () => { + const buf = g.Buffer.alloc(4) + expect(buf.length).toBe(4) + expect(Array.from(buf)).toEqual([0, 0, 0, 0]) + }) + + test("Buffer.concat joins multiple buffers", () => { + const a = g.Buffer.from("ab", "utf8") + const b = g.Buffer.from("cd", "utf8") + const result = g.Buffer.concat([a, b]) + expect(result.toString()).toBe("abcd") + }) + + test("Buffer.byteLength returns UTF-8 byte length", () => { + expect(g.Buffer.byteLength("hello")).toBe(5) + }) + + test("Buffer.isBuffer returns true for Buffer instances", () => { + const buf = g.Buffer.from("x", "utf8") + expect(g.Buffer.isBuffer(buf)).toBe(true) + expect(g.Buffer.isBuffer(new Uint8Array(1))).toBe(false) + }) + + // ── Idempotency ────────────────────────────────────────────────────── + + test("calling installBrowserPolyfills twice is safe", async () => { + const bridge = createNodeBridge() + await installBrowserPolyfills(bridge) + // Should still work + const result = g.crypto.createHash("sha256").update("test").digest("hex") + expect(result.length).toBe(64) + }) +}) diff --git a/packages/edge/js/tests/crypto.test.ts b/packages/edge/js/tests/crypto.test.ts new file mode 100644 index 0000000000..288a617603 --- /dev/null +++ b/packages/edge/js/tests/crypto.test.ts @@ -0,0 +1,125 @@ +/** + * Tests for the crypto polyfill (installCryptoPolyfill). + */ +import { describe, test, expect, beforeAll, afterAll } from "@jest/globals" +import { installCryptoPolyfill } from "../src/polyfills/crypto" +import type { RustBridge } from "../src/rust-bridge/types" +import { createHmac, createHash, randomBytes as nodeRandomBytes } from "crypto" + +// Create a real bridge backed by Node.js crypto for testing +function createNodeBridge(): RustBridge { + return { + hmacSha256(key, message) { + return new Uint8Array(createHmac("sha256", key).update(message).digest()) + }, + hmacSha256Hex(key, message) { + return createHmac("sha256", key).update(message).digest("hex") + }, + hmacSha512(key, message) { + return new Uint8Array(createHmac("sha512", key).update(message).digest()) + }, + hmacSha512Hex(key, message) { + return createHmac("sha512", key).update(message).digest("hex") + }, + hmac(algorithm, secret, data, encoding) { + return createHmac(algorithm, secret).update(data).digest(encoding as any) + }, + hash(algorithm, data, encoding) { + return createHash(algorithm).update(data).digest(encoding as any) + }, + randomBytesHex(size) { + return nodeRandomBytes(size).toString("hex") + }, + randomBytes(size) { + return new Uint8Array(nodeRandomBytes(size)) + }, + bufferToHex(data) { + return Buffer.from(data).toString("hex") + }, + bufferFromHex(hexStr) { + return new Uint8Array(Buffer.from(hexStr, "hex")) + }, + bufferToBase64(data) { + return Buffer.from(data).toString("base64") + }, + bufferFromBase64(b64) { + return new Uint8Array(Buffer.from(b64, "base64")) + }, + bufferByteLength(utf8Str) { + return Buffer.byteLength(utf8Str, "utf8") + }, + } +} + +describe("installCryptoPolyfill", () => { + const g = globalThis as any + let savedCrypto: any + + beforeAll(() => { + savedCrypto = g.crypto + // Wipe crypto to test installation from scratch + g.crypto = undefined + }) + + afterAll(() => { + g.crypto = savedCrypto + }) + + test("installs crypto.createHmac", () => { + const bridge = createNodeBridge() + installCryptoPolyfill(bridge) + + expect(typeof g.crypto.createHmac).toBe("function") + const result = g.crypto.createHmac("sha256", "secret").update("message").digest("hex") + expect(typeof result).toBe("string") + expect(result.length).toBe(64) + }) + + test("installs crypto.createHash", () => { + const result = g.crypto.createHash("sha256").update("hello world").digest("hex") + expect(result).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + }) + + test("createHmac supports chained updates", () => { + const polyfill = g.crypto.createHmac("sha256", "key") + .update("part1") + .update("part2") + .digest("hex") + + const native = createHmac("sha256", "key") + .update("part1part2") + .digest("hex") + + expect(polyfill).toBe(native) + }) + + test("createHash supports chained updates", () => { + const polyfill = g.crypto.createHash("sha256") + .update("a") + .update("b") + .digest("hex") + + const native = createHash("sha256") + .update("ab") + .digest("hex") + + expect(polyfill).toBe(native) + }) + + test("createHmac handles Uint8Array input", () => { + const data = new TextEncoder().encode("message") + const result = g.crypto.createHmac("sha256", "key").update(data).digest("hex") + + const expected = createHmac("sha256", "key").update("message").digest("hex") + expect(result).toBe(expected) + }) + + test("idempotent - calling twice doesn't break", () => { + const bridge = createNodeBridge() + installCryptoPolyfill(bridge) + installCryptoPolyfill(bridge) + + const result = g.crypto.createHmac("sha256", "s").update("d").digest("hex") + expect(result.length).toBe(64) + }) +}) diff --git a/packages/edge/js/tests/e2e-advanced.test.ts b/packages/edge/js/tests/e2e-advanced.test.ts new file mode 100644 index 0000000000..506134c016 --- /dev/null +++ b/packages/edge/js/tests/e2e-advanced.test.ts @@ -0,0 +1,646 @@ +/** + * Advanced end-to-end tests — stress, concurrency, encoding variants, + * cross-exchange consistency, fuzz, error boundaries, and full signing flows. + * + * These complement the basic e2e.test.ts with deeper coverage. + */ +import { describe, test, expect, beforeAll } from "@jest/globals" +import { createHmac, createHash, randomBytes as nodeRandomBytes } from "crypto" + +let ccxt: any +const g = globalThis as any + +beforeAll(async () => { + await import("../src/polyfills/node") + ccxt = (await import("ccxt")).default +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Cross-Exchange Signing Consistency +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Cross-Exchange Signing Consistency", () => { + const EXCHANGES = ["binance", "bybit", "okx", "kucoin", "bitget", "kraken"] + const TEST_PAYLOAD = "symbol=BTCUSDT×tamp=1700000000000&recvWindow=5000" + const TEST_SECRET = "exchange_api_secret_key_12345" + + test("all supported exchanges produce identical SHA-256 signatures for same input", () => { + const expected = createHmac("sha256", TEST_SECRET).update(TEST_PAYLOAD).digest("hex") + const results: Record = {} + + for (const id of EXCHANGES) { + try { + const ex = new ccxt[id]({ apiKey: "k", secret: "s" }) + results[id] = g.crypto.createHmac("sha256", TEST_SECRET).update(TEST_PAYLOAD).digest("hex") + } catch { + // exchange not available + } + } + + const values = Object.values(results) + expect(values.length).toBeGreaterThanOrEqual(4) + for (const sig of values) { + expect(sig).toBe(expected) + } + }) + + test("all supported exchanges produce identical SHA-512 signatures for same input", () => { + const expected = createHmac("sha512", TEST_SECRET).update(TEST_PAYLOAD).digest("hex") + + for (const id of EXCHANGES) { + try { + new ccxt[id]({ apiKey: "k", secret: "s" }) + const sig = g.crypto.createHmac("sha512", TEST_SECRET).update(TEST_PAYLOAD).digest("hex") + expect(sig).toBe(expected) + } catch { + // exchange not available + } + } + }) + + test("each exchange can independently sign after creation", () => { + const instances: any[] = [] + for (const id of EXCHANGES) { + try { + instances.push({ id, ex: new ccxt[id]({ apiKey: "k", secret: "s" }) }) + } catch { + // skip + } + } + expect(instances.length).toBeGreaterThanOrEqual(3) + + // Sign with each exchange and verify all produce correct results + for (const { id, ex } of instances) { + const msg = `exchange=${id}&ts=1700000000` + const sig = g.crypto.createHmac("sha256", "shared_secret").update(msg).digest("hex") + const expected = createHmac("sha256", "shared_secret").update(msg).digest("hex") + expect(sig).toBe(expected) + } + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Concurrent Async Signing +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Concurrent Async Signing", () => { + test("parallel Promise.all signing produces correct results", async () => { + const secret = "concurrent_secret" + const tasks = Array.from({ length: 100 }, (_, i) => { + return new Promise((resolve) => { + const msg = `nonce=${i}×tamp=${Date.now()}` + const sig = g.crypto.createHmac("sha256", secret).update(msg).digest("hex") + const expected = createHmac("sha256", secret).update(msg).digest("hex") + expect(sig).toBe(expected) + resolve() + }) + }) + await Promise.all(tasks) + }) + + test("interleaved HMAC builders don't cross-contaminate", () => { + const builder1 = g.crypto.createHmac("sha256", "secret1") + const builder2 = g.crypto.createHmac("sha256", "secret2") + const builder3 = g.crypto.createHmac("sha512", "secret3") + + // Interleave updates + builder1.update("msg1_part1") + builder2.update("msg2_part1") + builder3.update("msg3_part1") + builder1.update("&msg1_part2") + builder2.update("&msg2_part2") + builder3.update("&msg3_part2") + + const sig1 = builder1.digest("hex") + const sig2 = builder2.digest("hex") + const sig3 = builder3.digest("hex") + + expect(sig1).toBe(createHmac("sha256", "secret1").update("msg1_part1&msg1_part2").digest("hex")) + expect(sig2).toBe(createHmac("sha256", "secret2").update("msg2_part1&msg2_part2").digest("hex")) + expect(sig3).toBe(createHmac("sha512", "secret3").update("msg3_part1&msg3_part2").digest("hex")) + + // All three must be different + expect(new Set([sig1, sig2, sig3]).size).toBe(3) + }) + + test("rapid creation and signing of 200 exchange instances", () => { + const exchangeIds = ["binance", "bybit", "okx", "kucoin"] + const results: string[] = [] + + for (let i = 0; i < 200; i++) { + const id = exchangeIds[i % exchangeIds.length] + try { + new ccxt[id]({ apiKey: `key_${i}`, secret: `secret_${i}` }) + const sig = g.crypto.createHmac("sha256", `secret_${i}`).update(`payload_${i}`).digest("hex") + const expected = createHmac("sha256", `secret_${i}`).update(`payload_${i}`).digest("hex") + expect(sig).toBe(expected) + results.push(sig) + } catch { + // skip + } + } + // All should be unique (different secrets and payloads) + expect(new Set(results).size).toBe(results.length) + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Digest Encoding Variants +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Digest Encoding Variants", () => { + test("HMAC-SHA256 base64 encoding matches Node.js", () => { + const secret = "base64_test_secret" + const message = "symbol=BTCUSDT×tamp=1700000000000" + + // Node polyfill uses bridge.hmac fallback for base64 + const polyfill = g.crypto.createHmac("sha256", secret).update(message).digest("base64") + const native = createHmac("sha256", secret).update(message).digest("base64") + expect(polyfill).toBe(native) + }) + + test("HMAC-SHA512 base64 encoding matches Node.js", () => { + const polyfill = g.crypto.createHmac("sha512", "key").update("data").digest("base64") + const native = createHmac("sha512", "key").update("data").digest("base64") + expect(polyfill).toBe(native) + }) + + test("SHA-256 hash with hex and base64 both match Node.js", () => { + const data = "hash_me_please" + const hexPolyfill = g.crypto.createHash("sha256").update(data).digest("hex") + const hexNative = createHash("sha256").update(data).digest("hex") + expect(hexPolyfill).toBe(hexNative) + + const b64Polyfill = g.crypto.createHash("sha256").update(data).digest("base64") + const b64Native = createHash("sha256").update(data).digest("base64") + expect(b64Polyfill).toBe(b64Native) + }) + + test("MD5 hash matches Node.js", () => { + const polyfill = g.crypto.createHash("md5").update("test_data").digest("hex") + const native = createHash("md5").update("test_data").digest("hex") + expect(polyfill).toBe(native) + }) + + test("SHA-1 hash matches Node.js", () => { + const polyfill = g.crypto.createHash("sha1").update("test_data").digest("hex") + const native = createHash("sha1").update("test_data").digest("hex") + expect(polyfill).toBe(native) + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Fuzz Testing +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Fuzz Testing", () => { + test("500 random HMAC-SHA256 inputs match Node.js", () => { + for (let i = 0; i < 500; i++) { + const secretLen = Math.floor(Math.random() * 128) + 1 + const msgLen = Math.floor(Math.random() * 512) + 1 + const secret = nodeRandomBytes(secretLen).toString("hex") + const msg = nodeRandomBytes(msgLen).toString("hex") + + const polyfill = g.crypto.createHmac("sha256", secret).update(msg).digest("hex") + const native = createHmac("sha256", secret).update(msg).digest("hex") + expect(polyfill).toBe(native) + } + }) + + test("200 random HMAC-SHA512 inputs match Node.js", () => { + for (let i = 0; i < 200; i++) { + const secret = nodeRandomBytes(Math.floor(Math.random() * 64) + 1).toString("base64") + const msg = nodeRandomBytes(Math.floor(Math.random() * 256) + 1).toString("base64") + + const polyfill = g.crypto.createHmac("sha512", secret).update(msg).digest("hex") + const native = createHmac("sha512", secret).update(msg).digest("hex") + expect(polyfill).toBe(native) + } + }) + + test("200 random SHA-256 hash inputs match Node.js", () => { + for (let i = 0; i < 200; i++) { + const data = nodeRandomBytes(Math.floor(Math.random() * 1024) + 1).toString("hex") + const polyfill = g.crypto.createHash("sha256").update(data).digest("hex") + const native = createHash("sha256").update(data).digest("hex") + expect(polyfill).toBe(native) + } + }) + + test("100 random inputs with mixed chained updates", () => { + for (let i = 0; i < 100; i++) { + const secret = `fuzz_secret_${i}` + const numParts = Math.floor(Math.random() * 5) + 2 + const parts: string[] = [] + for (let j = 0; j < numParts; j++) { + parts.push(nodeRandomBytes(Math.floor(Math.random() * 64) + 1).toString("hex")) + } + + const chained = g.crypto.createHmac("sha256", secret) + for (const p of parts) chained.update(p) + const result = chained.digest("hex") + + const native = createHmac("sha256", secret).update(parts.join("")).digest("hex") + expect(result).toBe(native) + } + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Binary / Uint8Array Inputs +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Binary Uint8Array Inputs", () => { + test("HMAC with Uint8Array data matches string equivalent", () => { + const encoder = new TextEncoder() + const secret = "binary_test_secret" + const msgStr = "timestamp=1700000000000" + const msgBytes = encoder.encode(msgStr) + + const fromString = g.crypto.createHmac("sha256", secret).update(msgStr).digest("hex") + const fromBytes = g.crypto.createHmac("sha256", secret).update(msgBytes).digest("hex") + const native = createHmac("sha256", secret).update(msgStr).digest("hex") + + expect(fromString).toBe(native) + expect(fromBytes).toBe(native) + }) + + test("Hash with Uint8Array data matches string equivalent", () => { + const encoder = new TextEncoder() + const data = "hash this binary" + const bytes = encoder.encode(data) + + const fromString = g.crypto.createHash("sha256").update(data).digest("hex") + const fromBytes = g.crypto.createHash("sha256").update(bytes).digest("hex") + const native = createHash("sha256").update(data).digest("hex") + + expect(fromString).toBe(native) + expect(fromBytes).toBe(native) + }) + + test("mixed string and Uint8Array updates", () => { + const encoder = new TextEncoder() + const secret = "mixed_input_secret" + + const builder = g.crypto.createHmac("sha256", secret) + builder.update("part1=") + builder.update(encoder.encode("value1")) + builder.update("&part2=") + builder.update(encoder.encode("value2")) + const result = builder.digest("hex") + + const native = createHmac("sha256", secret).update("part1=value1&part2=value2").digest("hex") + expect(result).toBe(native) + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Multi-Encoding Buffer Roundtrips +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Multi-Encoding Buffer Roundtrips", () => { + test("utf8 -> hex -> base64 -> hex -> utf8 full cycle", () => { + const original = "Hello 世界! 🌍 Ünîcödé" + const buf1 = g.Buffer.from(original, "utf8") + const hex = buf1.toString("hex") + const buf2 = g.Buffer.from(hex, "hex") + const b64 = buf2.toString("base64") + const buf3 = g.Buffer.from(b64, "base64") + const hexAgain = buf3.toString("hex") + const buf4 = g.Buffer.from(hexAgain, "hex") + const final = buf4.toString("utf8") + expect(final).toBe(original) + }) + + test("binary/latin1 encoding preserves byte values", () => { + // Create a buffer with known bytes + const bytes = new Uint8Array([0, 1, 127, 128, 200, 255]) + const buf = g.Buffer.from(bytes) + const latin1 = buf.toString("binary") + expect(latin1.length).toBe(6) + expect(latin1.charCodeAt(0)).toBe(0) + expect(latin1.charCodeAt(2)).toBe(127) + expect(latin1.charCodeAt(3)).toBe(128) + expect(latin1.charCodeAt(5)).toBe(255) + + // Roundtrip + const restored = g.Buffer.from(latin1, "binary") + for (let i = 0; i < bytes.length; i++) { + expect(restored[i]).toBe(bytes[i]) + } + }) + + test("hex encoding handles all byte values (0x00-0xFF)", () => { + const bytes = new Uint8Array(256) + for (let i = 0; i < 256; i++) bytes[i] = i + const buf = g.Buffer.from(bytes) + const hex = buf.toString("hex") + expect(hex.length).toBe(512) + expect(hex.startsWith("000102")).toBe(true) + expect(hex.endsWith("fdfeff")).toBe(true) + + const restored = g.Buffer.from(hex, "hex") + for (let i = 0; i < 256; i++) { + expect(restored[i]).toBe(i) + } + }) + + test("base64 roundtrip with padding variations", () => { + // 0 padding chars + const buf3 = g.Buffer.from("abc", "utf8") + expect(g.Buffer.from(buf3.toString("base64"), "base64").toString("utf8")).toBe("abc") + + // 1 padding char (==) + const buf1 = g.Buffer.from("a", "utf8") + expect(g.Buffer.from(buf1.toString("base64"), "base64").toString("utf8")).toBe("a") + + // 2 padding chars (=) + const buf2 = g.Buffer.from("ab", "utf8") + expect(g.Buffer.from(buf2.toString("base64"), "base64").toString("utf8")).toBe("ab") + }) + + test("Buffer.from with ArrayBuffer input", () => { + const original = new Uint8Array([10, 20, 30, 40, 50]) + const buf = g.Buffer.from(original.buffer) + expect(buf.length).toBe(5) + expect(buf[0]).toBe(10) + expect(buf[4]).toBe(50) + expect(buf.toString("hex")).toBe("0a141e2832") + }) + + test("Buffer.concat with mixed sizes", () => { + const bufs = [ + g.Buffer.from("a", "utf8"), + g.Buffer.from("", "utf8"), + g.Buffer.from("bcd", "utf8"), + g.Buffer.from("", "utf8"), + g.Buffer.from("efghij", "utf8"), + ] + const result = g.Buffer.concat(bufs) + expect(result.toString("utf8")).toBe("abcdefghij") + expect(result.length).toBe(10) + }) + + test("Buffer.byteLength for multi-byte characters", () => { + // ASCII: 1 byte per char + expect(g.Buffer.byteLength("hello")).toBe(5) + // 2-byte chars (Latin extended) + expect(g.Buffer.byteLength("café")).toBe(5) // c=1, a=1, f=1, é=2 + // 3-byte chars (CJK) + expect(g.Buffer.byteLength("日本")).toBe(6) + // 4-byte chars (emoji) + expect(g.Buffer.byteLength("🌍")).toBe(4) + // Mixed + expect(g.Buffer.byteLength("Hi 🌍!")).toBe(8) + + // Verify against Node.js Buffer + expect(g.Buffer.byteLength("café")).toBe(Buffer.byteLength("café")) + expect(g.Buffer.byteLength("日本語")).toBe(Buffer.byteLength("日本語")) + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// randomBytes Advanced +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — randomBytes Stress", () => { + test("various sizes produce correct lengths", () => { + for (const size of [1, 2, 8, 16, 32, 64, 128, 256, 512, 1024]) { + const bytes = g.crypto.randomBytes(size) + expect(bytes.length).toBe(size) + } + }) + + test("1000 calls produce no duplicates (32 bytes each)", () => { + const seen = new Set() + for (let i = 0; i < 1000; i++) { + const hex = g.Buffer.from(g.crypto.randomBytes(32)).toString("hex") + expect(seen.has(hex)).toBe(false) + seen.add(hex) + } + }) + + test("randomBytes hex representation is valid lowercase hex", () => { + for (let i = 0; i < 100; i++) { + const hex = g.Buffer.from(g.crypto.randomBytes(16)).toString("hex") + expect(hex).toMatch(/^[0-9a-f]{32}$/) + } + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Signing Determinism +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Signing Determinism", () => { + test("same input always produces same output (1000 iterations)", () => { + const secret = "determinism_test_key" + const message = "symbol=BTCUSDT&side=BUY&quantity=0.001×tamp=1700000000000" + const expected = createHmac("sha256", secret).update(message).digest("hex") + + for (let i = 0; i < 1000; i++) { + const result = g.crypto.createHmac("sha256", secret).update(message).digest("hex") + expect(result).toBe(expected) + } + }) + + test("hash determinism across 500 iterations", () => { + const data = "deterministic_hash_test_data" + const expected = createHash("sha256").update(data).digest("hex") + + for (let i = 0; i < 500; i++) { + const result = g.crypto.createHash("sha256").update(data).digest("hex") + expect(result).toBe(expected) + } + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Full Exchange Signing Flow Simulations +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Exchange Signing Flow Simulations", () => { + test("Binance spot order with all parameters", () => { + const apiSecret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + const params: Record = { + symbol: "BTCUSDT", + side: "BUY", + type: "MARKET", + quoteOrderQty: "100", + newOrderRespType: "FULL", + recvWindow: "5000", + timestamp: "1700000000000", + } + const queryString = Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&") + const signature = g.crypto.createHmac("sha256", apiSecret).update(queryString).digest("hex") + const expected = createHmac("sha256", apiSecret).update(queryString).digest("hex") + + expect(signature).toBe(expected) + expect(signature.length).toBe(64) + + // Signed URL assembly + const signedUrl = `${queryString}&signature=${signature}` + expect(signedUrl).toContain("symbol=BTCUSDT") + expect(signedUrl).toContain(`signature=${expected}`) + }) + + test("Bybit v5 request signing simulation", () => { + const apiKey = "BYBIT_API_KEY" + const apiSecret = "BYBIT_SECRET_KEY" + const timestamp = "1700000000000" + const recvWindow = "5000" + + // Bybit signs: timestamp + apiKey + recvWindow + queryString + const queryString = "category=linear&symbol=BTCUSDT" + const signPayload = timestamp + apiKey + recvWindow + queryString + + const signature = g.crypto.createHmac("sha256", apiSecret).update(signPayload).digest("hex") + const expected = createHmac("sha256", apiSecret).update(signPayload).digest("hex") + expect(signature).toBe(expected) + }) + + test("OKX request signing simulation (base64 encoded HMAC-SHA256)", () => { + const secretKey = "OKX_SECRET_KEY" + const timestamp = "2023-11-15T10:00:00.000Z" + const method = "GET" + const requestPath = "/api/v5/account/balance" + + // OKX signs: timestamp + method + requestPath + body + const preSign = timestamp + method + requestPath + const signature = g.crypto.createHmac("sha256", secretKey).update(preSign).digest("base64") + const expected = createHmac("sha256", secretKey).update(preSign).digest("base64") + expect(signature).toBe(expected) + }) + + test("Kraken nonce-based signing simulation", () => { + const apiSecret = Buffer.from("kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==", "base64") + const nonce = "1616492376594" + const postData = `nonce=${nonce}&ordertype=limit&pair=XBTUSD&price=37500&type=buy&volume=1.25` + + // Kraken signs: SHA256(nonce + postData), then HMAC-SHA512 with base64-decoded secret + const sha256 = createHash("sha256").update(nonce + postData).digest() + const uriPath = "/0/private/AddOrder" + const message = Buffer.concat([Buffer.from(uriPath, "ascii"), sha256]) + + const signatureNative = createHmac("sha512", apiSecret).update(message).digest("base64") + + // Replicate with polyfill for the SHA256 step + const sha256Polyfill = g.crypto.createHash("sha256").update(nonce + postData).digest("hex") + const sha256Native = createHash("sha256").update(nonce + postData).digest("hex") + expect(sha256Polyfill).toBe(sha256Native) + }) + + test("multiple signing rounds with timestamp rotation", () => { + const secret = "rotating_timestamp_secret" + const baseTs = 1700000000000 + const signatures: string[] = [] + + for (let i = 0; i < 100; i++) { + const ts = baseTs + i * 1000 + const msg = `symbol=BTCUSDT&side=BUY&quantity=0.001×tamp=${ts}` + const sig = g.crypto.createHmac("sha256", secret).update(msg).digest("hex") + const expected = createHmac("sha256", secret).update(msg).digest("hex") + expect(sig).toBe(expected) + signatures.push(sig) + } + + // All signatures should be unique (different timestamps) + expect(new Set(signatures).size).toBe(100) + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Edge Cases & Error Boundaries +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Edge Cases", () => { + test("null bytes in message", () => { + const msg = "before\x00after" + const polyfill = g.crypto.createHmac("sha256", "secret").update(msg).digest("hex") + const native = createHmac("sha256", "secret").update(msg).digest("hex") + expect(polyfill).toBe(native) + }) + + test("newlines and control characters in message", () => { + const msg = "line1\nline2\rline3\tline4" + const polyfill = g.crypto.createHmac("sha256", "secret").update(msg).digest("hex") + const native = createHmac("sha256", "secret").update(msg).digest("hex") + expect(polyfill).toBe(native) + }) + + test("very long chained updates (100 parts)", () => { + const secret = "long_chain_secret" + const parts: string[] = [] + const builder = g.crypto.createHmac("sha256", secret) + + for (let i = 0; i < 100; i++) { + const part = `param${i}=value${i}&` + parts.push(part) + builder.update(part) + } + + const result = builder.digest("hex") + const expected = createHmac("sha256", secret).update(parts.join("")).digest("hex") + expect(result).toBe(expected) + }) + + test("unicode normalization edge cases", () => { + // é as single codepoint vs e + combining accent + const composed = "caf\u00e9" + const decomposed = "cafe\u0301" + + const sig1 = g.crypto.createHmac("sha256", "key").update(composed).digest("hex") + const sig2 = g.crypto.createHmac("sha256", "key").update(decomposed).digest("hex") + + // These are different byte sequences, so signatures should differ + expect(sig1).not.toBe(sig2) + + // But each should match Node.js + expect(sig1).toBe(createHmac("sha256", "key").update(composed).digest("hex")) + expect(sig2).toBe(createHmac("sha256", "key").update(decomposed).digest("hex")) + }) + + test("empty Buffer operations", () => { + const empty = g.Buffer.from("", "utf8") + expect(empty.length).toBe(0) + expect(empty.toString("hex")).toBe("") + expect(empty.toString("base64")).toBe("") + expect(empty.toString("utf8")).toBe("") + + // concat with empty buffers + const result = g.Buffer.concat([empty, g.Buffer.from("x", "utf8"), empty]) + expect(result.toString("utf8")).toBe("x") + }) + + test("Buffer.alloc large size", () => { + const buf = g.Buffer.alloc(1024 * 1024) // 1MB + expect(buf.length).toBe(1024 * 1024) + expect(buf[0]).toBe(0) + expect(buf[buf.length - 1]).toBe(0) + }) + + test("signing with JSON body payloads", () => { + const secret = "json_body_secret" + const body = JSON.stringify({ + symbol: "BTCUSDT", + side: "BUY", + type: "LIMIT", + quantity: "0.001", + price: "50000.00", + timeInForce: "GTC", + }) + + const sig = g.crypto.createHmac("sha256", secret).update(body).digest("hex") + const expected = createHmac("sha256", secret).update(body).digest("hex") + expect(sig).toBe(expected) + }) + + test("signing with URL-encoded special characters", () => { + const secret = "urlencoded_secret" + const msg = "symbol=BTC%2FUSDT¬e=hello%20world&special=%26%3D%3F" + + const sig = g.crypto.createHmac("sha256", secret).update(msg).digest("hex") + const expected = createHmac("sha256", secret).update(msg).digest("hex") + expect(sig).toBe(expected) + }) +}) diff --git a/packages/edge/js/tests/e2e.test.ts b/packages/edge/js/tests/e2e.test.ts new file mode 100644 index 0000000000..6a64cc04ce --- /dev/null +++ b/packages/edge/js/tests/e2e.test.ts @@ -0,0 +1,311 @@ +/** + * End-to-end tests — verify the full polyfill stack works with real ccxt. + * + * Simple tests: basic crypto, buffer, polyfill availability. + * Advanced tests: multi-exchange signing, chained updates, cross-algorithm + * consistency, binary secrets, concurrent usage, full signing flow. + */ +import { describe, test, expect, beforeAll } from "@jest/globals" +import { createHmac, createHash, randomBytes as nodeRandomBytes } from "crypto" + +let ccxt: any +const g = globalThis as any + +beforeAll(async () => { + await import("../src/polyfills/node") + ccxt = (await import("ccxt")).default +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Simple E2E Tests +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Simple E2E", () => { + test("polyfills are installed", () => { + expect(typeof g.crypto.createHmac).toBe("function") + expect(typeof g.crypto.createHash).toBe("function") + expect(typeof g.crypto.randomBytes).toBe("function") + expect(typeof g.Buffer).toBe("function") + }) + + test("HMAC-SHA256 matches Node.js crypto", () => { + const polyfill = g.crypto.createHmac("sha256", "secret").update("hello").digest("hex") + const native = createHmac("sha256", "secret").update("hello").digest("hex") + expect(polyfill).toBe(native) + }) + + test("HMAC-SHA512 matches Node.js crypto", () => { + const polyfill = g.crypto.createHmac("sha512", "key").update("data").digest("hex") + const native = createHmac("sha512", "key").update("data").digest("hex") + expect(polyfill).toBe(native) + }) + + test("SHA-256 hash matches Node.js crypto", () => { + const polyfill = g.crypto.createHash("sha256").update("test").digest("hex") + const native = createHash("sha256").update("test").digest("hex") + expect(polyfill).toBe(native) + }) + + test("Buffer roundtrip: string -> hex -> base64 -> string", () => { + const original = "Hello, OctoBot Edge!" + const buf = g.Buffer.from(original, "utf8") + const hex = buf.toString("hex") + const fromHex = g.Buffer.from(hex, "hex") + const b64 = fromHex.toString("base64") + const fromB64 = g.Buffer.from(b64, "base64") + expect(fromB64.toString("utf8")).toBe(original) + }) + + test("randomBytes returns unique values", () => { + const a = g.crypto.randomBytes(32) + const b = g.crypto.randomBytes(32) + expect(a.length).toBe(32) + expect(b.length).toBe(32) + // Overwhelmingly likely to be different + expect(g.Buffer.from(a).toString("hex")).not.toBe(g.Buffer.from(b).toString("hex")) + }) + + test("Binance HMAC test vector", () => { + const secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + const message = + "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" + const result = g.crypto.createHmac("sha256", secret).update(message).digest("hex") + expect(result).toBe("c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71") + }) +}) + +// ═══════════════════════════════════════════════════════════════════════════ +// Advanced E2E Tests +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Advanced E2E — Chained Updates", () => { + test("HMAC with multiple updates matches single update", () => { + const secret = "multi_update_secret" + const parts = ["part1=value1", "&part2=value2", "&part3=value3"] + + const chained = g.crypto.createHmac("sha256", secret) + for (const p of parts) chained.update(p) + const chainedResult = chained.digest("hex") + + const single = g.crypto.createHmac("sha256", secret) + .update(parts.join("")) + .digest("hex") + + const native = createHmac("sha256", secret) + .update(parts.join("")) + .digest("hex") + + expect(chainedResult).toBe(single) + expect(chainedResult).toBe(native) + }) + + test("Hash with incremental updates matches full input", () => { + const data = "The quick brown fox jumps over the lazy dog" + const words = data.split(" ") + + const incremental = g.crypto.createHash("sha256") + words.forEach((w: string, i: number) => { + incremental.update(i > 0 ? " " + w : w) + }) + const result = incremental.digest("hex") + + const native = createHash("sha256").update(data).digest("hex") + expect(result).toBe(native) + }) +}) + +describe("Advanced E2E — Cross-Algorithm Consistency", () => { + test("SHA-256 and SHA-512 produce different-length results", () => { + const sha256 = g.crypto.createHmac("sha256", "s").update("d").digest("hex") + const sha512 = g.crypto.createHmac("sha512", "s").update("d").digest("hex") + expect(sha256.length).toBe(64) + expect(sha512.length).toBe(128) + expect(sha256).not.toBe(sha512.substring(0, 64)) + }) + + test("different secrets produce different signatures", () => { + const msg = "same_message" + const sigs = new Set() + for (let i = 0; i < 50; i++) { + const sig = g.crypto.createHmac("sha256", `secret_${i}`).update(msg).digest("hex") + sigs.add(sig) + } + expect(sigs.size).toBe(50) + }) + + test("different messages produce different signatures", () => { + const secret = "same_secret" + const sigs = new Set() + for (let i = 0; i < 50; i++) { + const sig = g.crypto.createHmac("sha256", secret).update(`msg_${i}`).digest("hex") + sigs.add(sig) + } + expect(sigs.size).toBe(50) + }) +}) + +describe("Advanced E2E — Binary and Edge-Case Secrets", () => { + test("empty message and secret", () => { + const polyfill = g.crypto.createHmac("sha256", "").update("").digest("hex") + const native = createHmac("sha256", "").update("").digest("hex") + expect(polyfill).toBe(native) + }) + + test("very long secret (4KB)", () => { + const longSecret = "k".repeat(4096) + const polyfill = g.crypto.createHmac("sha256", longSecret).update("data").digest("hex") + const native = createHmac("sha256", longSecret).update("data").digest("hex") + expect(polyfill).toBe(native) + }) + + test("very long message (1MB)", () => { + const longMsg = "x".repeat(1024 * 1024) + const polyfill = g.crypto.createHmac("sha256", "secret").update(longMsg).digest("hex") + const native = createHmac("sha256", "secret").update(longMsg).digest("hex") + expect(polyfill).toBe(native) + }) + + test("special characters in secret and message", () => { + const secret = "key+with/special=chars&%20" + const message = "data=hello world&price=1000€" + const polyfill = g.crypto.createHmac("sha256", secret).update(message).digest("hex") + const native = createHmac("sha256", secret).update(message).digest("hex") + expect(polyfill).toBe(native) + }) +}) + +describe("Advanced E2E — CCXT Exchange Integration", () => { + test("ccxt exchange instances are created with polyfilled crypto", () => { + const exchange = new ccxt.binance({ + apiKey: "test_key", + secret: "test_secret", + }) + expect(exchange).toBeDefined() + expect(typeof exchange.sign).toBe("function") + expect(typeof exchange.fetchTicker).toBe("function") + }) + + test("polyfilled crypto.createHmac matches ccxt signing expectations", () => { + // ccxt 4.x uses noble-hashes internally for exchange.hmac(), + // but polyfills patch crypto.createHmac which older ccxt or + // custom signing code uses. Verify both produce the same result. + const secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + const message = + "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" + + const polyfillResult = g.crypto.createHmac("sha256", secret).update(message).digest("hex") + const nativeResult = createHmac("sha256", secret).update(message).digest("hex") + + expect(polyfillResult).toBe(nativeResult) + expect(polyfillResult).toBe("c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71") + }) + + test("multiple exchange instances can coexist", () => { + const exchangeIds = ["binance", "bybit", "okx", "kucoin"] + const exchanges = exchangeIds.map( + (id) => new ccxt[id]({ apiKey: "k", secret: "s" }), + ) + + for (const ex of exchanges) { + expect(ex).toBeDefined() + expect(typeof ex.sign).toBe("function") + } + + // Signing via polyfill should be consistent + const msg = "test_payload" + const secret = "test_secret" + const expected = createHmac("sha256", secret).update(msg).digest("hex") + const polyfillResult = g.crypto.createHmac("sha256", secret).update(msg).digest("hex") + expect(polyfillResult).toBe(expected) + }) + + test("Binance official signature via polyfilled crypto", () => { + const apiSecret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + + const params: Record = { + symbol: "LTCBTC", + side: "BUY", + type: "LIMIT", + timeInForce: "GTC", + quantity: "1", + price: "0.1", + recvWindow: "5000", + timestamp: "1499827319559", + } + const queryString = Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&") + const signature = g.crypto.createHmac("sha256", apiSecret).update(queryString).digest("hex") + + expect(signature).toBe("c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71") + }) + + test("rapid successive signatures via polyfill are correct and unique", () => { + const secret = "rapid_test_secret" + let prev = "" + for (let i = 0; i < 500; i++) { + const msg = `nonce=${i}` + const sig = g.crypto.createHmac("sha256", secret).update(msg).digest("hex") + const expected = createHmac("sha256", secret).update(msg).digest("hex") + expect(sig).toBe(expected) + expect(sig).not.toBe(prev) + prev = sig + } + }) +}) + +describe("Advanced E2E — Buffer Integration", () => { + test("Buffer.concat produces correct result", () => { + const a = g.Buffer.from("hello", "utf8") + const b = g.Buffer.from(" ", "utf8") + const c = g.Buffer.from("world", "utf8") + const result = g.Buffer.concat([a, b, c]) + expect(result.toString("utf8")).toBe("hello world") + }) + + test("Buffer.alloc creates zeroed buffer", () => { + const buf = g.Buffer.alloc(16) + expect(buf.length).toBe(16) + for (let i = 0; i < 16; i++) { + expect(buf[i]).toBe(0) + } + }) + + test("Buffer.isBuffer identifies buffers", () => { + const buf = g.Buffer.from("test", "utf8") + expect(g.Buffer.isBuffer(buf)).toBe(true) + expect(g.Buffer.isBuffer(new Uint8Array(4))).toBe(false) + expect(g.Buffer.isBuffer("string")).toBe(false) + }) + + test("Buffer.byteLength matches Node.js", () => { + const str = "Hello, World! 日本語" + const polyfillLen = g.Buffer.byteLength(str) + const nodeLen = Buffer.byteLength(str) + expect(polyfillLen).toBe(nodeLen) + }) +}) + +describe("Advanced E2E — URLSearchParams", () => { + test("basic query string parsing", () => { + const params = new g.URLSearchParams("foo=bar&baz=qux") + expect(params.get("foo")).toBe("bar") + expect(params.get("baz")).toBe("qux") + }) + + test("empty value handling", () => { + const params = new g.URLSearchParams("key=&other=value") + expect(params.get("key")).toBe("") + expect(params.get("other")).toBe("value") + }) + + test("key with equals sign in value", () => { + const params = new g.URLSearchParams("data=a=b=c&other=d") + expect(params.get("data")).toBe("a=b=c") + expect(params.get("other")).toBe("d") + }) + + test("toString roundtrip", () => { + const original = "a=1&b=2&c=3" + const params = new g.URLSearchParams(original) + expect(params.toString()).toBe(original) + }) +}) diff --git a/packages/edge/js/tests/encoding.test.ts b/packages/edge/js/tests/encoding.test.ts new file mode 100644 index 0000000000..07da8c9dec --- /dev/null +++ b/packages/edge/js/tests/encoding.test.ts @@ -0,0 +1,133 @@ +import { describe, test, expect, beforeAll, afterAll } from '@jest/globals'; +import { installEncodingPolyfill } from '../src/polyfills/encoding'; + +describe('TextEncoder/TextDecoder polyfill', () => { + // Save originals and remove them to force polyfill installation + const origEncoder = globalThis.TextEncoder; + const origDecoder = globalThis.TextDecoder; + + beforeAll(() => { + (globalThis as any).TextEncoder = undefined; + (globalThis as any).TextDecoder = undefined; + installEncodingPolyfill(); + }); + + afterAll(() => { + (globalThis as any).TextEncoder = origEncoder; + (globalThis as any).TextDecoder = origDecoder; + }); + + describe('TextEncoder', () => { + test('encodes ASCII correctly', () => { + const encoder = new globalThis.TextEncoder(); + const result = encoder.encode('hello'); + expect(Array.from(result)).toEqual([0x68, 0x65, 0x6c, 0x6c, 0x6f]); + }); + + test('encodes 2-byte UTF-8 (Latin characters)', () => { + const encoder = new globalThis.TextEncoder(); + // é = U+00E9 → 0xC3 0xA9 + const result = encoder.encode('é'); + expect(Array.from(result)).toEqual([0xc3, 0xa9]); + }); + + test('encodes 3-byte UTF-8 (CJK characters)', () => { + const encoder = new globalThis.TextEncoder(); + // 中 = U+4E2D → 0xE4 0xB8 0xAD + const result = encoder.encode('中'); + expect(Array.from(result)).toEqual([0xe4, 0xb8, 0xad]); + }); + + test('encodes 4-byte UTF-8 (emoji)', () => { + const encoder = new globalThis.TextEncoder(); + // 😀 = U+1F600 → 0xF0 0x9F 0x98 0x80 + const result = encoder.encode('😀'); + expect(Array.from(result)).toEqual([0xf0, 0x9f, 0x98, 0x80]); + }); + + test('encodes mixed ASCII and multi-byte', () => { + const encoder = new globalThis.TextEncoder(); + const result = encoder.encode('a中😀'); + expect(Array.from(result)).toEqual([ + 0x61, // 'a' + 0xe4, 0xb8, 0xad, // '中' + 0xf0, 0x9f, 0x98, 0x80, // '😀' + ]); + }); + + test('matches native TextEncoder output', () => { + const native = new origEncoder(); + const polyfill = new globalThis.TextEncoder(); + const testStrings = ['hello', 'café', '日本語', '🎉🎊', 'a\u0000b', '']; + for (const s of testStrings) { + expect(Array.from(polyfill.encode(s))).toEqual(Array.from(native.encode(s))); + } + }); + }); + + describe('TextDecoder', () => { + test('decodes ASCII correctly', () => { + const decoder = new globalThis.TextDecoder(); + const result = decoder.decode(new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f])); + expect(result).toBe('hello'); + }); + + test('decodes 2-byte UTF-8', () => { + const decoder = new globalThis.TextDecoder(); + const result = decoder.decode(new Uint8Array([0xc3, 0xa9])); + expect(result).toBe('é'); + }); + + test('decodes 3-byte UTF-8 (CJK)', () => { + const decoder = new globalThis.TextDecoder(); + const result = decoder.decode(new Uint8Array([0xe4, 0xb8, 0xad])); + expect(result).toBe('中'); + }); + + test('decodes 4-byte UTF-8 (emoji)', () => { + const decoder = new globalThis.TextDecoder(); + const result = decoder.decode(new Uint8Array([0xf0, 0x9f, 0x98, 0x80])); + expect(result).toBe('😀'); + }); + + test('handles truncated sequences with replacement char', () => { + const decoder = new globalThis.TextDecoder(); + // Truncated 2-byte sequence: just the lead byte + const result = decoder.decode(new Uint8Array([0xc3])); + expect(result).toBe('\ufffd'); + }); + + test('handles invalid continuation bytes', () => { + const decoder = new globalThis.TextDecoder(); + // 0xc3 expects a continuation byte, but 0x41 is ASCII + const result = decoder.decode(new Uint8Array([0xc3, 0x41])); + expect(result).toBe('\ufffd' + 'A'); + }); + + test('roundtrips with TextEncoder', () => { + const encoder = new globalThis.TextEncoder(); + const decoder = new globalThis.TextDecoder(); + const testStrings = ['hello', 'café', '日本語テスト', '🎉 party 🎊', '']; + for (const s of testStrings) { + expect(decoder.decode(encoder.encode(s))).toBe(s); + } + }); + + test('roundtrip matches native implementation', () => { + const nativeEnc = new origEncoder(); + const nativeDec = new origDecoder(); + const polyEnc = new globalThis.TextEncoder(); + const polyDec = new globalThis.TextDecoder(); + + const testStrings = ['hello world', 'Ünïcödé', '漢字', '🇺🇸🏳️‍🌈', 'mixed café 中文 😀']; + for (const s of testStrings) { + // Polyfill encode → polyfill decode + expect(polyDec.decode(polyEnc.encode(s))).toBe(s); + // Native encode → polyfill decode + expect(polyDec.decode(nativeEnc.encode(s))).toBe(s); + // Polyfill encode → native decode + expect(nativeDec.decode(polyEnc.encode(s))).toBe(s); + } + }); + }); +}); diff --git a/packages/edge/js/tests/exchange.integration.test.ts b/packages/edge/js/tests/exchange.integration.test.ts new file mode 100644 index 0000000000..05fc9ee52d --- /dev/null +++ b/packages/edge/js/tests/exchange.integration.test.ts @@ -0,0 +1,254 @@ +/** + * Integration tests for @octobot/edge exchange patching. + * + * These tests exercise the real ccxt library with our patching layer, + * validating that HMAC signing produces correct results across exchanges. + * The Rust bridge is NOT loaded (no native addon in test env), so we + * verify the fallback path and patching mechanics. + */ +import { describe, test, expect, beforeAll } from '@jest/globals'; +import ccxt from 'ccxt'; +import { createHmac } from 'crypto'; + +// We import patch directly since init() requires the native bridge +import { patchExchange } from '../src/exchange/patch'; +import type { RustBridge } from '../src/rust-bridge/types'; + + +// ── Mock Rust bridge using Node.js crypto ────────────────────────────────── + +function createMockBridge(): RustBridge { + const enc = new TextEncoder(); + return { + hmac(algorithm: string, secret: string, data: string, encoding: string): string { + return createHmac(algorithm, secret).update(data).digest(encoding as any); + }, + hash(algorithm: string, data: string, encoding: string): string { + const { createHash } = require('crypto'); + return createHash(algorithm).update(data).digest(encoding as any); + }, + hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array { + return new Uint8Array(createHmac('sha256', key).update(message).digest()); + }, + hmacSha256Hex(key: Uint8Array, message: Uint8Array): string { + return createHmac('sha256', key).update(message).digest('hex'); + }, + hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array { + return new Uint8Array(createHmac('sha512', key).update(message).digest()); + }, + hmacSha512Hex(key: Uint8Array, message: Uint8Array): string { + return createHmac('sha512', key).update(message).digest('hex'); + }, + randomBytesHex(size: number): string { + const { randomBytes } = require('crypto'); + return randomBytes(size).toString('hex'); + }, + randomBytes(size: number): Uint8Array { + const { randomBytes } = require('crypto'); + return new Uint8Array(randomBytes(size)); + }, + bufferToHex(data: Uint8Array): string { + return Buffer.from(data).toString('hex'); + }, + bufferFromHex(hexStr: string): Uint8Array { + return new Uint8Array(Buffer.from(hexStr, 'hex')); + }, + bufferToBase64(data: Uint8Array): string { + return Buffer.from(data).toString('base64'); + }, + bufferFromBase64(b64: string): Uint8Array { + return new Uint8Array(Buffer.from(b64, 'base64')); + }, + bufferByteLength(utf8Str: string): number { + return Buffer.byteLength(utf8Str, 'utf8'); + }, + }; +} + +// ── Exchanges to test ────────────────────────────────────────────────────── + +const EXCHANGES = ['binance', 'bybit', 'okx', 'kucoin', 'bitget', 'kraken'] as const; + +// ── Test vectors ─────────────────────────────────────────────────────────── + +const BINANCE_SECRET = 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j'; +const BINANCE_QUERY = 'symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559'; +const BINANCE_EXPECTED = 'c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71'; + +describe('Exchange integration with mock bridge', () => { + let bridge: RustBridge; + + beforeAll(() => { + bridge = createMockBridge(); + }); + + describe('patchExchange with real ccxt exchanges', () => { + test.each(EXCHANGES)('%s: patched hmac matches Node.js crypto for sha256/hex', (exchangeId) => { + const ExchangeClass = (ccxt as any)[exchangeId]; + const patched = new ExchangeClass({ apiKey: 'test', secret: 'test' }); + + patchExchange(patched as any, bridge); + + const request = 'timestamp=1700000000000&symbol=BTCUSDT'; + const secret = 'my_secret_key'; + + const expected = createHmac('sha256', secret).update(request).digest('hex'); + const accelerated = patched.hmac(request, secret, 'sha256', 'hex'); + + expect(accelerated).toBe(expected); + }); + + test.each(EXCHANGES)('%s: patched hmac matches Node.js crypto for sha512/hex', (exchangeId) => { + const ExchangeClass = (ccxt as any)[exchangeId]; + const patched = new ExchangeClass({ apiKey: 'test', secret: 'test' }); + + patchExchange(patched as any, bridge); + + const request = 'timestamp=1700000000000'; + const secret = 'my_secret_key'; + + const expected = createHmac('sha512', secret).update(request).digest('hex'); + const accelerated = patched.hmac(request, secret, 'sha512', 'hex'); + + expect(accelerated).toBe(expected); + }); + + test.each(EXCHANGES)('%s: exchange has standard ccxt methods', (exchangeId) => { + const ExchangeClass = (ccxt as any)[exchangeId]; + const exchange = new ExchangeClass({}); + + expect(typeof exchange.fetchTicker).toBe('function'); + expect(typeof exchange.fetchOrderBook).toBe('function'); + expect(typeof exchange.fetchOHLCV).toBe('function'); + expect(typeof exchange.createOrder).toBe('function'); + expect(typeof exchange.cancelOrder).toBe('function'); + expect(typeof exchange.fetchBalance).toBe('function'); + }); + + test.each(EXCHANGES)('%s: config is forwarded correctly', (exchangeId) => { + const ExchangeClass = (ccxt as any)[exchangeId]; + const exchange = new ExchangeClass({ timeout: 7777, rateLimit: 500 }); + + expect(exchange.timeout).toBe(7777); + expect(exchange.rateLimit).toBe(500); + }); + }); + + describe('Binance signature test vectors', () => { + test('matches official Binance API documentation vector', () => { + const exchange: any = new ccxt.binance({ apiKey: 'test', secret: BINANCE_SECRET }); + patchExchange(exchange as any, bridge); + + const result = exchange.hmac(BINANCE_QUERY, BINANCE_SECRET, 'sha256', 'hex'); + expect(result).toBe(BINANCE_EXPECTED); + }); + + test('matches with reversed argument order (secret as key)', () => { + // Verify the patched hmac correctly uses secret as HMAC key + const expected = createHmac('sha256', BINANCE_SECRET) + .update(BINANCE_QUERY) + .digest('hex'); + expect(expected).toBe(BINANCE_EXPECTED); + }); + }); + + describe('signing consistency', () => { + test('repeated calls produce identical results', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const results = new Set(); + for (let i = 0; i < 100; i++) { + results.add(exchange.hmac('data', 'secret', 'sha256', 'hex')); + } + expect(results.size).toBe(1); + }); + + test('different inputs produce different outputs', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const results = new Set(); + for (let i = 0; i < 50; i++) { + results.add(exchange.hmac(`timestamp=${i}`, 'secret', 'sha256', 'hex')); + } + expect(results.size).toBe(50); + }); + + test('hmac output is valid 64-char hex', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const result = exchange.hmac('data', 'secret', 'sha256', 'hex'); + expect(result).toMatch(/^[0-9a-f]{64}$/); + }); + + test('sha512 hmac output is valid 128-char hex', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const result = exchange.hmac('data', 'secret', 'sha512', 'hex'); + expect(result).toMatch(/^[0-9a-f]{128}$/); + }); + }); + + describe('edge cases', () => { + test('unicode in request data', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const result = exchange.hmac('note=日本語テスト🎉', 'secret', 'sha256', 'hex'); + expect(result).toMatch(/^[0-9a-f]{64}$/); + }); + + test('very long request string', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const longRequest = 'x'.repeat(100_000); + const result = exchange.hmac(longRequest, 'secret', 'sha256', 'hex'); + expect(result).toMatch(/^[0-9a-f]{64}$/); + }); + + test('empty request and secret', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const result = exchange.hmac('', '', 'sha256', 'hex'); + const expected = createHmac('sha256', '').update('').digest('hex'); + expect(result).toBe(expected); + }); + + test('special characters in secret', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + patchExchange(exchange as any, bridge); + + const secret = 'key+with/special=chars&and%20encoding'; + const result = exchange.hmac('data', secret, 'sha256', 'hex'); + const expected = createHmac('sha256', secret).update('data').digest('hex'); + expect(result).toBe(expected); + }); + }); + + describe('null bridge (graceful degradation)', () => { + test('patchExchange with null bridge does nothing', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + const originalHmac = exchange.hmac; + + patchExchange(exchange as any, null); + + // hmac should be unchanged + expect(exchange.hmac).toBe(originalHmac); + }); + + test('unpatched exchange hmac is unchanged after null bridge patch', () => { + const exchange: any = new ccxt.binance({ apiKey: 'k', secret: 's' }); + const hmacBefore = exchange.hmac; + + patchExchange(exchange as any, null); + + // hmac reference should not have changed + expect(exchange.hmac).toBe(hmacBefore); + }); + }); +}); diff --git a/packages/edge/js/tests/polyfills.node.test.ts b/packages/edge/js/tests/polyfills.node.test.ts new file mode 100644 index 0000000000..882a2be454 --- /dev/null +++ b/packages/edge/js/tests/polyfills.node.test.ts @@ -0,0 +1,49 @@ +import { describe, test, expect, beforeAll } from '@jest/globals'; +import { createHash as nodeCreateHash, createHmac as nodeCreateHmac, randomBytes as nodeRandomBytes } from 'crypto'; + +describe('Node polyfills', () => { + beforeAll(async () => { + await import('../src/polyfills/node'); + }); + + test('crypto.createHmac supports chained updates', () => { + const g = globalThis as any; + const actual = g.crypto + .createHmac('sha256', 'secret') + .update('hello ') + .update('world') + .digest('hex'); + + const expected = nodeCreateHmac('sha256', 'secret').update('hello ').update('world').digest('hex'); + expect(actual).toBe(expected); + }); + + test('crypto.createHash supports chained updates', () => { + const g = globalThis as any; + const actual = g.crypto + .createHash('sha256') + .update('hello ') + .update('world') + .digest('hex'); + + const expected = nodeCreateHash('sha256').update('hello ').update('world').digest('hex'); + expect(actual).toBe(expected); + }); + + test('crypto.randomBytes matches requested length', () => { + const g = globalThis as any; + const len = 32; + const a = g.crypto.randomBytes(len); + const b = nodeRandomBytes(len); + + expect(a).toBeInstanceOf(Buffer); + expect(a.length).toBe(len); + expect(b.length).toBe(len); + }); + + test('Buffer is available globally', () => { + const g = globalThis as any; + const buf = g.Buffer.from('ccxt', 'utf8'); + expect(buf.toString('hex')).toBe('63637874'); + }); +}); diff --git a/packages/edge/js/tests/runtime-env.test.ts b/packages/edge/js/tests/runtime-env.test.ts new file mode 100644 index 0000000000..5f94515fba --- /dev/null +++ b/packages/edge/js/tests/runtime-env.test.ts @@ -0,0 +1,321 @@ +/** + * Runtime environment simulation tests. + * + * Simulates WASM, React Native, and Deno environments by manipulating + * globalThis to verify runtime detection and polyfill behavior. + */ +import { describe, test, expect, beforeEach, afterEach, afterAll } from '@jest/globals'; +import { detectRuntime } from '../src/polyfills/detect'; +import { installEncodingPolyfill } from '../src/polyfills/encoding'; +import { createHmac, createHash, randomBytes } from 'crypto'; + +// Save original globalThis state +const originalDeno = (globalThis as any).Deno; +const originalBun = (globalThis as any).Bun; +const originalWindow = (globalThis as any).window; +const originalNavigator = (globalThis as any).navigator; +const originalTextEncoder = globalThis.TextEncoder; +const originalTextDecoder = globalThis.TextDecoder; + +function cleanGlobalThis() { + delete (globalThis as any).Deno; + delete (globalThis as any).Bun; + delete (globalThis as any).window; + // navigator may not be deletable in all envs + try { (globalThis as any).navigator = undefined; } catch {} +} + +function restoreGlobalThis() { + if (originalDeno !== undefined) (globalThis as any).Deno = originalDeno; + else delete (globalThis as any).Deno; + if (originalBun !== undefined) (globalThis as any).Bun = originalBun; + else delete (globalThis as any).Bun; + if (originalWindow !== undefined) (globalThis as any).window = originalWindow; + else delete (globalThis as any).window; + if (originalNavigator !== undefined) (globalThis as any).navigator = originalNavigator; + (globalThis as any).TextEncoder = originalTextEncoder; + (globalThis as any).TextDecoder = originalTextDecoder; +} + +// ── Runtime detection tests ──────────────────────────────────────────────── + +describe('Runtime detection', () => { + beforeEach(cleanGlobalThis); + afterEach(restoreGlobalThis); + + test('detects Node.js by default', () => { + expect(detectRuntime()).toBe('node'); + }); + + test('detects Deno when globalThis.Deno exists', () => { + (globalThis as any).Deno = { version: { deno: '1.40.0' } }; + expect(detectRuntime()).toBe('deno'); + }); + + test('detects Bun when globalThis.Bun exists', () => { + (globalThis as any).Bun = { version: '1.0.0' }; + expect(detectRuntime()).toBe('bun'); + }); + + test('detects browser when window exists', () => { + (globalThis as any).window = {}; + expect(detectRuntime()).toBe('browser'); + }); + + test('detects React Native when navigator.product is ReactNative', () => { + (globalThis as any).navigator = { product: 'ReactNative' }; + expect(detectRuntime()).toBe('react-native'); + }); + + test('Deno takes priority over window', () => { + (globalThis as any).Deno = {}; + (globalThis as any).window = {}; + expect(detectRuntime()).toBe('deno'); + }); + + test('Bun takes priority over window', () => { + (globalThis as any).Bun = {}; + (globalThis as any).window = {}; + expect(detectRuntime()).toBe('bun'); + }); +}); + +// ── React Native polyfill simulation ─────────────────────────────────────── + +describe('React Native environment simulation', () => { + beforeEach(() => { + cleanGlobalThis(); + (globalThis as any).navigator = { product: 'ReactNative' }; + }); + + afterEach(restoreGlobalThis); + + test('runtime detected as react-native', () => { + expect(detectRuntime()).toBe('react-native'); + }); + + test('TextEncoder polyfill works when native is missing', () => { + // Simulate Hermes: no TextEncoder + (globalThis as any).TextEncoder = undefined; + (globalThis as any).TextDecoder = undefined; + + installEncodingPolyfill(); + + const encoder = new globalThis.TextEncoder(); + + // ASCII + expect(Array.from(encoder.encode('hello'))).toEqual([0x68, 0x65, 0x6c, 0x6c, 0x6f]); + + // Multi-byte (emoji — 4-byte UTF-8) + expect(Array.from(encoder.encode('😀'))).toEqual([0xf0, 0x9f, 0x98, 0x80]); + + // CJK (3-byte UTF-8) + expect(Array.from(encoder.encode('中'))).toEqual([0xe4, 0xb8, 0xad]); + }); + + test('TextDecoder polyfill roundtrips correctly', () => { + (globalThis as any).TextEncoder = undefined; + (globalThis as any).TextDecoder = undefined; + + installEncodingPolyfill(); + + const encoder = new globalThis.TextEncoder(); + const decoder = new globalThis.TextDecoder(); + + const testStrings = [ + 'hello world', + 'café au lait', + '日本語テスト', + '🎉🚀💻🎊', + 'mixed ASCII 中文 and émoji 🎉', + '', + 'a\0b', // null byte + ]; + + for (const s of testStrings) { + const encoded = encoder.encode(s); + const decoded = decoder.decode(encoded); + expect(decoded).toBe(s); + } + }); + + test('TextEncoder polyfill matches native for exchange signing payloads', () => { + (globalThis as any).TextEncoder = undefined; + (globalThis as any).TextDecoder = undefined; + + installEncodingPolyfill(); + + const polyfillEncoder = new globalThis.TextEncoder(); + + // Typical exchange API payloads + const payloads = [ + 'symbol=BTCUSDT&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=50000.00×tamp=1700000000000', + 'api_key=abc123def456&sign=abcdef0123456789', + 'GET\n/api/v5/account/balance\n1700000000.000\n', + 'timestamp=1700000000000&recvWindow=5000', + ]; + + for (const payload of payloads) { + const polyfillResult = polyfillEncoder.encode(payload); + const nativeResult = originalTextEncoder ? new originalTextEncoder().encode(payload) : polyfillResult; + expect(Array.from(polyfillResult)).toEqual(Array.from(nativeResult)); + } + }); +}); + +// ── WASM environment simulation ──────────────────────────────────────────── + +describe('WASM/browser environment simulation', () => { + beforeEach(() => { + cleanGlobalThis(); + (globalThis as any).window = {}; + }); + + afterEach(restoreGlobalThis); + + test('runtime detected as browser', () => { + expect(detectRuntime()).toBe('browser'); + }); + + test('crypto polyfills can create HMAC in browser-like env', async () => { + // The node polyfills module installs crypto.createHmac on globalThis + // This simulates what happens when the WASM bridge loads + await import('../src/polyfills/node'); + + const g = globalThis as any; + const result = g.crypto + .createHmac('sha256', 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j') + .update('symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559') + .digest('hex'); + + expect(result).toBe('c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71'); + }); + + test('crypto polyfills produce correct sha512 HMAC', async () => { + await import('../src/polyfills/node'); + + const g = globalThis as any; + const result = g.crypto + .createHmac('sha512', 'secret_key') + .update('test_message') + .digest('hex'); + + const expected = createHmac('sha512', 'secret_key').update('test_message').digest('hex'); + expect(result).toBe(expected); + }); + + test('crypto polyfills support hash chaining', async () => { + await import('../src/polyfills/node'); + + const g = globalThis as any; + const result = g.crypto + .createHash('sha256') + .update('part1') + .update('part2') + .update('part3') + .digest('hex'); + + const expected = createHash('sha256').update('part1').update('part2').update('part3').digest('hex'); + expect(result).toBe(expected); + }); + + test('Buffer polyfill is available', async () => { + await import('../src/polyfills/node'); + + const g = globalThis as any; + expect(g.Buffer).toBeDefined(); + + // Test Buffer operations used by ccxt + const b = g.Buffer.from('68656c6c6f', 'hex'); + expect(b.toString('utf8')).toBe('hello'); + + const b2 = g.Buffer.from('hello', 'utf8'); + expect(b2.toString('hex')).toBe('68656c6c6f'); + + const b3 = g.Buffer.from('aGVsbG8=', 'base64'); + expect(b3.toString('utf8')).toBe('hello'); + }); + + test('randomBytes produces correct length', async () => { + await import('../src/polyfills/node'); + + const g = globalThis as any; + for (const len of [16, 32, 48, 64]) { + const bytes = g.crypto.randomBytes(len); + expect(bytes.length).toBe(len); + } + }); +}); + +// ── Deno environment simulation ──────────────────────────────────────────── + +describe('Deno environment simulation', () => { + beforeEach(() => { + cleanGlobalThis(); + (globalThis as any).Deno = { version: { deno: '2.0.0' } }; + }); + + afterEach(restoreGlobalThis); + + test('runtime detected as deno', () => { + expect(detectRuntime()).toBe('deno'); + }); + + test('Deno has native TextEncoder so polyfill is not needed', () => { + // In real Deno, TextEncoder exists natively + // Just verify our detection picks deno runtime + expect(globalThis.TextEncoder).toBeDefined(); + }); +}); + +// ── Cross-environment HMAC consistency ───────────────────────────────────── + +describe('Cross-environment signing consistency', () => { + const SECRET = 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j'; + const QUERY = 'symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559'; + const EXPECTED = 'c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71'; + + test('Node crypto produces correct Binance signature', () => { + const result = createHmac('sha256', SECRET).update(QUERY).digest('hex'); + expect(result).toBe(EXPECTED); + }); + + test('TextEncoder-based signing matches crypto-based signing', () => { + // This is what the bridge does: encode strings to Uint8Array then HMAC + const enc = new TextEncoder(); + const keyBytes = enc.encode(SECRET); + const msgBytes = enc.encode(QUERY); + + // Node's createHmac accepts Uint8Array + const result = createHmac('sha256', keyBytes).update(msgBytes).digest('hex'); + expect(result).toBe(EXPECTED); + }); + + test('Uint8Array key produces same result as string key for ASCII', () => { + const enc = new TextEncoder(); + + // This verifies our patching approach: we encode(secret) and encode(request) + // which must produce the same HMAC as using raw strings + const stringResult = createHmac('sha256', SECRET).update(QUERY).digest('hex'); + const bytesResult = createHmac('sha256', enc.encode(SECRET)) + .update(enc.encode(QUERY)) + .digest('hex'); + + expect(bytesResult).toBe(stringResult); + }); + + test('multiple exchange secrets produce unique signatures', () => { + const secrets = [ + 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j', + 't7T0YlFnYXk0Fx3JswQsDrViLg1Gh3DUU5Mr', + 'kQwJkAx1rvnmEd9mLqPZBjXhxYIjVHMO', + 'VGhpcyBpcyBhIHRlc3Qgc2VjcmV0IGtleQ==', + ]; + + const results = new Set( + secrets.map((s) => createHmac('sha256', s).update(QUERY).digest('hex')) + ); + + expect(results.size).toBe(secrets.length); + }); +}); diff --git a/packages/edge/js/tsconfig.json b/packages/edge/js/tsconfig.json new file mode 100644 index 0000000000..65b7bfc5a6 --- /dev/null +++ b/packages/edge/js/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/edge/js/tsup.config.ts b/packages/edge/js/tsup.config.ts new file mode 100644 index 0000000000..32634ae5db --- /dev/null +++ b/packages/edge/js/tsup.config.ts @@ -0,0 +1,61 @@ +import { defineConfig } from "tsup" + +export default defineConfig([ + // ESM for Node / Bun / Deno + { + entry: { + index: "src/index.ts", + "polyfills/node": "src/polyfills/node.ts", + "polyfills/browser": "src/polyfills/browser.ts", + "polyfills/crypto": "src/polyfills/crypto.ts", + }, + format: ["esm"], + outDir: "dist/esm", + dts: true, + splitting: false, + sourcemap: true, + target: "node20", + external: ["ccxt"], + }, + // CJS for Node require() + { + entry: { + index: "src/index.ts", + "polyfills/node": "src/polyfills/node.ts", + }, + format: ["cjs"], + outDir: "dist/cjs", + dts: false, + splitting: false, + sourcemap: true, + target: "node20", + external: ["ccxt"], + }, + // Browser bundle (no Node built-ins, WASM-only Rust bridge) + { + entry: { + index: "src/index.ts", + "polyfills/browser": "src/polyfills/browser.ts", + }, + format: ["esm"], + outDir: "dist/browser", + outExtension: () => ({ js: ".mjs" }), + dts: false, + platform: "browser", + env: { RUNTIME: "browser" }, + external: ["ccxt"], + }, + // React Native (CommonJS, no crypto.subtle assumption) + { + entry: { + index: "src/index.ts", + "polyfills/mobile": "src/polyfills/mobile.ts", + }, + format: ["cjs"], + outDir: "dist/rn", + dts: false, + platform: "neutral", + env: { RUNTIME: "react-native" }, + external: ["ccxt", "react-native"], + }, +]) diff --git a/packages/edge/js/ubrn.config.yaml b/packages/edge/js/ubrn.config.yaml new file mode 100644 index 0000000000..ae32b2b5ff --- /dev/null +++ b/packages/edge/js/ubrn.config.yaml @@ -0,0 +1,10 @@ +rust: + directory: ../rust/mobile + manifestPath: Cargo.toml +bindings: + ios: + directory: ios + iosDeploymentTarget: "15.1" + android: + directory: android/app/src/main/jni +outputDir: src/generated/octobot_edge_mobile diff --git a/packages/edge/octobot_edge/__init__.py b/packages/edge/octobot_edge/__init__.py new file mode 100644 index 0000000000..100d56b3bb --- /dev/null +++ b/packages/edge/octobot_edge/__init__.py @@ -0,0 +1,14 @@ +VERSION = "0.1.0" +PROJECT_NAME = "OctoBot-Edge" + +from octobot_edge.crypto import hmac_sha256, hmac_sha512, maybe_rust_hmac +from octobot_edge.exchange.client import create_exchange + +__all__ = [ + "VERSION", + "PROJECT_NAME", + "hmac_sha256", + "hmac_sha512", + "maybe_rust_hmac", + "create_exchange", +] diff --git a/packages/edge/octobot_edge/_native.pyi b/packages/edge/octobot_edge/_native.pyi new file mode 100644 index 0000000000..0702cb8b0e --- /dev/null +++ b/packages/edge/octobot_edge/_native.pyi @@ -0,0 +1,5 @@ +def hmac_sha256(key: bytes, message: bytes) -> bytes: ... +def hmac_sha512(key: bytes, message: bytes) -> bytes: ... +def hmac(algorithm: str, secret: str, data: str, encoding: str) -> str: ... +def hash(algorithm: str, data: str, encoding: str) -> str: ... +def hash_digest(algorithm: str, data: str, encoding: str) -> str: ... diff --git a/packages/edge/octobot_edge/crypto/__init__.py b/packages/edge/octobot_edge/crypto/__init__.py new file mode 100644 index 0000000000..190c542bae --- /dev/null +++ b/packages/edge/octobot_edge/crypto/__init__.py @@ -0,0 +1,3 @@ +from octobot_edge.crypto.hmac import hmac_sha256, hmac_sha512, maybe_rust_hmac + +__all__ = ["hmac_sha256", "hmac_sha512", "maybe_rust_hmac"] diff --git a/packages/edge/octobot_edge/crypto/hmac.py b/packages/edge/octobot_edge/crypto/hmac.py new file mode 100644 index 0000000000..3af363320b --- /dev/null +++ b/packages/edge/octobot_edge/crypto/hmac.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import hmac as _hmac +import hashlib + +try: + from octobot_edge._native import hmac_sha256 as _rust_hmac_sha256 + from octobot_edge._native import hmac_sha512 as _rust_hmac_sha512 + _HAS_RUST = True +except ImportError: + _HAS_RUST = False + + +def maybe_rust_hmac( + key: bytes, message: bytes, *, algorithm: str = "sha256" +) -> bytes | None: + """Return HMAC bytes via Rust extension, or None if unavailable. + + Supports "sha256" and "sha512". + """ + if _HAS_RUST: + if algorithm == "sha256": + return _rust_hmac_sha256(key, message) + if algorithm == "sha512": + return _rust_hmac_sha512(key, message) + return None + + +def hmac_sha256(key: bytes, message: bytes) -> bytes: + """Always-available HMAC-SHA256 (stdlib fallback).""" + if _HAS_RUST: + return _rust_hmac_sha256(key, message) + return _hmac.new(key, message, hashlib.sha256).digest() + + +def hmac_sha512(key: bytes, message: bytes) -> bytes: + """Always-available HMAC-SHA512 (stdlib fallback).""" + if _HAS_RUST: + return _rust_hmac_sha512(key, message) + return _hmac.new(key, message, hashlib.sha512).digest() diff --git a/packages/edge/octobot_edge/exchange/__init__.py b/packages/edge/octobot_edge/exchange/__init__.py new file mode 100644 index 0000000000..439a01ee27 --- /dev/null +++ b/packages/edge/octobot_edge/exchange/__init__.py @@ -0,0 +1,3 @@ +from octobot_edge.exchange.client import create_exchange + +__all__ = ["create_exchange"] diff --git a/packages/edge/octobot_edge/exchange/client.py b/packages/edge/octobot_edge/exchange/client.py new file mode 100644 index 0000000000..1dfc369f13 --- /dev/null +++ b/packages/edge/octobot_edge/exchange/client.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import hashlib +from base64 import b64encode + +import ccxt +import ccxt.async_support as ccxt_async +from octobot_edge.crypto.hmac import maybe_rust_hmac + + +def create_exchange( + exchange_id: str, config: dict, *, async_mode: bool = False +) -> ccxt.Exchange: + """Return a ccxt exchange instance with optional Rust-backed signing.""" + cls = getattr(ccxt_async if async_mode else ccxt, exchange_id) + instance = cls(config) + _patch_signing(instance) + return instance + + +def _normalize_algorithm(algorithm) -> str | None: + """Convert algorithm argument (hashlib module or string) to a lowercase string.""" + if algorithm is None: + return "sha256" + if algorithm is hashlib.sha256: + return "sha256" + if algorithm is hashlib.sha512: + return "sha512" + if isinstance(algorithm, str): + return algorithm.lower() + return None + + +def _patch_signing(exchange: ccxt.Exchange) -> None: + """Override ccxt hmac() with Rust implementation when available.""" + original = exchange.hmac + + def fast_hmac(request, secret, algorithm=None, digest="hex"): + algo = _normalize_algorithm(algorithm) + if algo in ("sha256", "sha512"): + result = maybe_rust_hmac( + secret.encode() if isinstance(secret, str) else secret, + request.encode() if isinstance(request, str) else request, + algorithm=algo, + ) + if result is not None: + if digest == "hex": + return result.hex() + if digest == "base64": + return b64encode(result).decode("ascii") + # For other digest formats (e.g. None -> raw bytes), return raw + return result + return original(request, secret, algorithm, digest) + + exchange.hmac = fast_hmac diff --git a/packages/edge/octobot_edge/exchange/normalize.py b/packages/edge/octobot_edge/exchange/normalize.py new file mode 100644 index 0000000000..71894d4435 --- /dev/null +++ b/packages/edge/octobot_edge/exchange/normalize.py @@ -0,0 +1,44 @@ +"""Unified data schema normalization for exchange responses.""" +from __future__ import annotations + +from typing import Any + + +def normalize_ticker(raw: dict[str, Any]) -> dict[str, Any]: + """Normalize a raw ticker response into a unified schema.""" + return { + "symbol": raw.get("symbol"), + "last": _to_float(raw.get("last")), + "bid": _to_float(raw.get("bid")), + "ask": _to_float(raw.get("ask")), + "high": _to_float(raw.get("high")), + "low": _to_float(raw.get("low")), + "volume": _to_float(raw.get("baseVolume")), + "timestamp": raw.get("timestamp"), + } + + +def normalize_order(raw: dict[str, Any]) -> dict[str, Any]: + """Normalize a raw order response into a unified schema.""" + return { + "id": raw.get("id"), + "symbol": raw.get("symbol"), + "side": raw.get("side"), + "type": raw.get("type"), + "price": _to_float(raw.get("price")), + "amount": _to_float(raw.get("amount")), + "filled": _to_float(raw.get("filled")), + "remaining": _to_float(raw.get("remaining")), + "status": raw.get("status"), + "timestamp": raw.get("timestamp"), + } + + +def _to_float(value: Any) -> float | None: + """Safely convert a value to float, returning None on failure.""" + if value is None: + return None + try: + return float(value) + except (ValueError, TypeError): + return None diff --git a/packages/edge/pyproject.toml b/packages/edge/pyproject.toml new file mode 100644 index 0000000000..63c64eaa6c --- /dev/null +++ b/packages/edge/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "octobot-edge" +version = "0.1.0" +description = "Universal CCXT wrapper with Rust-accelerated signing" +requires-python = ">=3.10" +dependencies = [ + "ccxt>=4.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", +] + +[tool.setuptools.packages.find] +include = ["octobot_edge*"] diff --git a/packages/edge/requirements.txt b/packages/edge/requirements.txt new file mode 100644 index 0000000000..af05ac8ae0 --- /dev/null +++ b/packages/edge/requirements.txt @@ -0,0 +1 @@ +ccxt>=4.0.0 diff --git a/packages/edge/rust/Cargo.lock b/packages/edge/rust/Cargo.lock new file mode 100644 index 0000000000..3dca581007 --- /dev/null +++ b/packages/edge/rust/Cargo.lock @@ -0,0 +1,2073 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags 2.11.0", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "octobot-edge-core" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "hex", + "hmac", + "md-5", + "rand", + "reqwest", + "serde", + "serde_json", + "sha2", + "sha3", + "thiserror", +] + +[[package]] +name = "octobot-edge-mobile" +version = "0.1.0" +dependencies = [ + "octobot-edge-core", + "thiserror", + "tokio", + "uniffi", +] + +[[package]] +name = "octobot-edge-napi" +version = "0.1.0" +dependencies = [ + "hex", + "napi", + "napi-build", + "napi-derive", + "octobot-edge-core", +] + +[[package]] +name = "octobot-edge-wasm" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "getrandom", + "hex", + "js-sys", + "octobot-edge-core", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "octobot_edge_native" +version = "0.1.0" +dependencies = [ + "octobot-edge-core", + "pyo3", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "textwrap", + "toml", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/edge/rust/Cargo.toml b/packages/edge/rust/Cargo.toml new file mode 100644 index 0000000000..34f7f0adbe --- /dev/null +++ b/packages/edge/rust/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +resolver = "2" +members = ["core", "wasm", "napi", "py", "mobile"] + +[workspace.dependencies] +hmac = "0.12" +sha2 = "0.10" +sha3 = "0.10" +md-5 = "0.10" +base64 = { version = "0.22", features = ["alloc"] } +hex = "0.4" +rand = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "1" diff --git a/packages/edge/rust/core/Cargo.toml b/packages/edge/rust/core/Cargo.toml new file mode 100644 index 0000000000..c97298c441 --- /dev/null +++ b/packages/edge/rust/core/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "octobot-edge-core" +version = "0.1.0" +edition = "2021" + +[lib] +name = "octobot_edge_core" + +[dependencies] +hmac = { workspace = true } +sha2 = { workspace = true } +sha3 = { workspace = true } +md-5 = { workspace = true } +base64 = { workspace = true } +hex = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] } diff --git a/packages/edge/rust/core/src/lib.rs b/packages/edge/rust/core/src/lib.rs new file mode 100644 index 0000000000..d8a3d82a8c --- /dev/null +++ b/packages/edge/rust/core/src/lib.rs @@ -0,0 +1,537 @@ +use hmac::{Hmac, Mac}; +use sha2::{Sha256, Sha384, Sha512, Digest as Sha2Digest}; +use sha3::{Sha3_256, Sha3_512}; +use md5::Md5; +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; + +// ── Error type ─────────────────────────────────────────────────────────────── + +#[derive(Debug, thiserror::Error)] +pub enum PolyfillError { + #[error("Unsupported algorithm: {0}")] + UnsupportedAlgorithm(String), + #[error("HTTP error: {0}")] + Http(String), + #[error("Invalid encoding: {0}")] + Encoding(String), +} + +// ── Fetch types ────────────────────────────────────────────────────────────── + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FetchRequest { + pub url: String, + pub method: String, + pub headers: Vec<(String, String)>, + pub body: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FetchResponse { + pub status: u16, + pub ok: bool, + pub body: String, + pub headers: Vec<(String, String)>, +} + +// ── Encoding helper ────────────────────────────────────────────────────────── + +fn encode_bytes(bytes: &[u8], encoding: &str) -> Result { + match encoding { + "hex" => Ok(hex::encode(bytes)), + "base64" => Ok(BASE64.encode(bytes)), + // Latin-1: each byte maps to a Unicode code point 0–255 (matching Node.js "binary" encoding) + "binary" | "latin1" => Ok(bytes.iter().map(|&b| b as char).collect()), + other => Err(PolyfillError::Encoding(format!("unsupported encoding: {other}"))), + } +} + +// ── HMAC ───────────────────────────────────────────────────────────────────── + +/// Compute HMAC with the given algorithm, returning the result in the specified encoding. +/// Supports sha256, sha512, sha384. +pub fn hmac( + algorithm: &str, + secret: &str, + data: &str, + encoding: &str, +) -> Result { + let key = secret.as_bytes(); + let msg = data.as_bytes(); + + let result: Vec = match algorithm.to_lowercase().as_str() { + "sha256" => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|e| PolyfillError::UnsupportedAlgorithm(e.to_string()))?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + "sha512" => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|e| PolyfillError::UnsupportedAlgorithm(e.to_string()))?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + "sha384" => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|e| PolyfillError::UnsupportedAlgorithm(e.to_string()))?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + other => return Err(PolyfillError::UnsupportedAlgorithm(other.to_string())), + }; + + encode_bytes(&result, encoding) +} + +/// Compute HMAC-SHA256 from raw byte slices, returning raw bytes. +/// Used by Python and NAPI bindings for direct byte-level HMAC. +pub fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, PolyfillError> { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|e| PolyfillError::UnsupportedAlgorithm(e.to_string()))?; + mac.update(message); + Ok(mac.finalize().into_bytes().to_vec()) +} + +/// Compute HMAC-SHA512 from raw byte slices, returning raw bytes. +pub fn hmac_sha512(key: &[u8], message: &[u8]) -> Result, PolyfillError> { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|e| PolyfillError::UnsupportedAlgorithm(e.to_string()))?; + mac.update(message); + Ok(mac.finalize().into_bytes().to_vec()) +} + +// ── Hash ───────────────────────────────────────────────────────────────────── + +/// Compute a hash digest. Supports sha256, sha512, sha384, sha3-256, sha3-512, md5. +pub fn hash( + algorithm: &str, + data: &str, + encoding: &str, +) -> Result { + let bytes = data.as_bytes(); + + let result: Vec = match algorithm.to_lowercase().as_str() { + "sha256" => Sha256::digest(bytes).to_vec(), + "sha512" => Sha512::digest(bytes).to_vec(), + "sha384" => Sha384::digest(bytes).to_vec(), + "sha3-256" => Sha3_256::digest(bytes).to_vec(), + "sha3-512" => Sha3_512::digest(bytes).to_vec(), + "md5" => Md5::digest(bytes).to_vec(), + other => return Err(PolyfillError::UnsupportedAlgorithm(other.to_string())), + }; + + encode_bytes(&result, encoding) +} + +// ── Random bytes ───────────────────────────────────────────────────────────── + +/// Maximum allowed size for random byte generation (1 MiB). +const MAX_RANDOM_BYTES: usize = 1_048_576; + +pub fn random_bytes(size: usize) -> Result, PolyfillError> { + if size > MAX_RANDOM_BYTES { + return Err(PolyfillError::Encoding( + format!("random_bytes size {size} exceeds maximum {MAX_RANDOM_BYTES}"), + )); + } + let mut bytes = vec![0u8; size]; + rand::thread_rng().fill_bytes(&mut bytes); + Ok(bytes) +} + +pub fn random_bytes_hex(size: usize) -> Result { + Ok(hex::encode(random_bytes(size)?)) +} + +// ── Buffer ops ─────────────────────────────────────────────────────────────── + +pub fn buffer_from_hex(hex_str: &str) -> Result, PolyfillError> { + hex::decode(hex_str).map_err(|e| PolyfillError::Encoding(e.to_string())) +} + +pub fn buffer_to_hex(data: &[u8]) -> String { + hex::encode(data) +} + +pub fn buffer_from_base64(b64: &str) -> Result, PolyfillError> { + BASE64.decode(b64).map_err(|e| PolyfillError::Encoding(e.to_string())) +} + +pub fn buffer_to_base64(data: &[u8]) -> String { + BASE64.encode(data) +} + +pub fn buffer_concat(bufs: Vec>) -> Vec { + let total: usize = bufs.iter().map(|b| b.len()).sum(); + let mut result = Vec::with_capacity(total); + for buf in bufs { + result.extend_from_slice(&buf); + } + result +} + +pub fn buffer_byte_length(utf8_str: &str) -> usize { + utf8_str.len() +} + +// ── Fetch (blocking — works on iOS/Android native, not WASM) ───────────────── + +#[cfg(not(target_arch = "wasm32"))] +pub fn fetch_sync(req: FetchRequest) -> Result { + // Validate URL scheme to prevent SSRF against local/internal resources + let url: reqwest::Url = req.url.parse() + .map_err(|e| PolyfillError::Http(format!("invalid URL: {e}")))?; + match url.scheme() { + "https" | "http" => {} + scheme => return Err(PolyfillError::Http( + format!("unsupported URL scheme: {scheme} (only http/https allowed)"), + )), + } + + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(false) + .redirect(reqwest::redirect::Policy::limited(5)) + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| PolyfillError::Http(e.to_string()))?; + + let method: reqwest::Method = req.method.parse() + .map_err(|e| PolyfillError::Http(format!("invalid HTTP method: {e}")))?; + + let mut builder = client.request(method, url); + + for (k, v) in &req.headers { + // Reject header names/values containing CRLF to prevent header injection + if k.contains('\r') || k.contains('\n') || k.contains('\0') { + return Err(PolyfillError::Http( + format!("invalid header name: contains prohibited characters"), + )); + } + if v.contains('\r') || v.contains('\n') || v.contains('\0') { + return Err(PolyfillError::Http( + format!("invalid header value: contains prohibited characters"), + )); + } + builder = builder.header(k.as_str(), v.as_str()); + } + + if let Some(body) = req.body { + builder = builder.body(body); + } + + let response = builder.send().map_err(|e| PolyfillError::Http(e.to_string()))?; + let status = response.status().as_u16(); + let ok = response.status().is_success(); + let headers: Vec<(String, String)> = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + // Limit response body to 10 MiB to prevent OOM from malicious servers. + // Read as raw bytes first so we can enforce the limit even for chunked responses + // (which lack a Content-Length header). + const MAX_RESPONSE_BODY: usize = 10 * 1024 * 1024; + if let Some(len) = response.content_length() { + if len > MAX_RESPONSE_BODY as u64 { + return Err(PolyfillError::Http( + format!("response body too large: {len} bytes (max {MAX_RESPONSE_BODY})"), + )); + } + } + let bytes = response.bytes().map_err(|e| PolyfillError::Http(e.to_string()))?; + if bytes.len() > MAX_RESPONSE_BODY { + return Err(PolyfillError::Http( + format!("response body too large: {} bytes (max {MAX_RESPONSE_BODY})", bytes.len()), + )); + } + let body = String::from_utf8_lossy(&bytes).into_owned(); + + Ok(FetchResponse { status, ok, body, headers }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hmac_sha256_hex() { + let result = hmac("sha256", "secret_key", "message_data", "hex").unwrap(); + assert_eq!(result.len(), 64); + assert!(result.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_hmac_sha512_hex() { + let result = hmac("sha512", "secret_key", "message_data", "hex").unwrap(); + assert_eq!(result.len(), 128); + } + + #[test] + fn test_hmac_sha256_bytes() { + let result = hmac_sha256(b"key", b"message").unwrap(); + assert_eq!(result.len(), 32); + } + + #[test] + fn test_hmac_sha512_bytes() { + let result = hmac_sha512(b"key", b"message").unwrap(); + assert_eq!(result.len(), 64); + } + + #[test] + fn test_hash_sha256() { + let result = hash("sha256", "hello world", "hex").unwrap(); + assert_eq!(result, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"); + } + + #[test] + fn test_hash_md5() { + let result = hash("md5", "hello world", "hex").unwrap(); + assert_eq!(result, "5eb63bbbe01eeed093cb22bb8f5acdc3"); + } + + #[test] + fn test_random_bytes() { + let a = random_bytes(16).unwrap(); + let b = random_bytes(16).unwrap(); + assert_eq!(a.len(), 16); + assert_ne!(a, b); + } + + #[test] + fn test_random_bytes_zero() { + let r = random_bytes(0).unwrap(); + assert!(r.is_empty()); + } + + #[test] + fn test_random_bytes_exceeds_max() { + assert!(random_bytes(2_000_000).is_err()); + } + + #[test] + fn test_buffer_hex_roundtrip() { + let original = b"hello"; + let hex_str = buffer_to_hex(original); + let decoded = buffer_from_hex(&hex_str).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn test_buffer_base64_roundtrip() { + let original = b"hello world"; + let b64 = buffer_to_base64(original); + let decoded = buffer_from_base64(&b64).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn test_binance_signature() { + // Known test vector from Binance API docs + let secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"; + let query = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559"; + let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"; + let result = hmac("sha256", secret, query, "hex").unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_unsupported_algorithm() { + assert!(hmac("blake2", "key", "msg", "hex").is_err()); + assert!(hash("blake2", "data", "hex").is_err()); + } + + // ── Additional hash algorithm vectors ────────────────────────────── + + #[test] + fn test_hmac_sha384_hex() { + let result = hmac("sha384", "secret_key", "message_data", "hex").unwrap(); + assert_eq!(result.len(), 96); // SHA384 = 48 bytes = 96 hex chars + } + + #[test] + fn test_hash_sha512() { + let result = hash("sha512", "hello world", "hex").unwrap(); + assert_eq!( + result, + "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f\ + 989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f" + ); + } + + #[test] + fn test_hash_sha384() { + let result = hash("sha384", "hello world", "hex").unwrap(); + assert_eq!( + result, + "fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3\ + e417cb71ce646efd0819dd8c088de1bd" + ); + } + + #[test] + fn test_hash_sha3_256() { + let result = hash("sha3-256", "hello world", "hex").unwrap(); + assert_eq!( + result, + "644bcc7e564373040999aac89e7622f3ca71fba1d972fd94a31c3bfbf24e3938" + ); + } + + #[test] + fn test_hash_sha3_512() { + let result = hash("sha3-512", "hello world", "hex").unwrap(); + assert_eq!( + result, + "840006653e9ac9e95117a15c915caab81662918e925de9e004f774ff82d7079a\ + 40d4d27b1b372657c61d46d470304c88c788b3a4527ad074d1dccbee5dbaa99a" + ); + } + + #[test] + fn test_hmac_base64_encoding() { + let result = hmac("sha256", "key", "data", "base64").unwrap(); + // base64 output should be non-empty and valid + assert!(!result.is_empty()); + assert!(base64::engine::general_purpose::STANDARD.decode(&result).is_ok()); + } + + // ── Error path tests ────────────────────────────────────────────── + + #[test] + fn test_unsupported_encoding() { + let err = hmac("sha256", "key", "msg", "foobar").unwrap_err(); + match err { + PolyfillError::Encoding(_) => {} // correct variant + other => panic!("expected Encoding error, got: {other}"), + } + } + + #[test] + fn test_binary_encoding_latin1() { + // "binary" / "latin1" encoding maps each byte to a Unicode code point 0–255, + // matching Node.js behavior. Non-UTF-8 bytes should now succeed. + let non_utf8 = vec![0xFF, 0xFE, 0x80, 0x90]; + let result = encode_bytes(&non_utf8, "binary").unwrap(); + assert_eq!(result, "\u{FF}\u{FE}\u{80}\u{90}"); + + // "latin1" alias should produce the same result + assert_eq!(encode_bytes(&non_utf8, "latin1").unwrap(), result); + + // ASCII bytes should pass through unchanged + let valid = b"hello"; + assert_eq!(encode_bytes(valid, "binary").unwrap(), "hello"); + } + + #[test] + fn test_buffer_from_hex_invalid() { + assert!(buffer_from_hex("xyz!!!").is_err()); + } + + #[test] + fn test_buffer_from_base64_invalid() { + assert!(buffer_from_base64("!!!!").is_err()); + } + + // ── Buffer edge cases ───────────────────────────────────────────── + + #[test] + fn test_buffer_concat_empty() { + assert!(buffer_concat(vec![]).is_empty()); + } + + #[test] + fn test_buffer_concat_mixed() { + let result = buffer_concat(vec![vec![], vec![1, 2], vec![], vec![3]]); + assert_eq!(result, vec![1, 2, 3]); + } + + #[test] + fn test_buffer_byte_length_multibyte() { + assert_eq!(buffer_byte_length(""), 0); + assert_eq!(buffer_byte_length("hello"), 5); + assert_eq!(buffer_byte_length("中"), 3); // 3-byte UTF-8 + } + + // ── Binance HMAC via byte-level API ────────────────────────────── + + #[test] + fn test_binance_signature_via_hmac_sha256() { + // Same Binance test vector, via hmac_sha256 byte API + let secret = b"NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"; + let query = b"symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559"; + let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"; + let result = hmac_sha256(secret, query).unwrap(); + assert_eq!(hex::encode(result), expected); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_fetch_sync_rejects_file_scheme() { + let req = FetchRequest { + url: "file:///etc/passwd".to_string(), + method: "GET".to_string(), + headers: vec![], + body: None, + }; + let err = fetch_sync(req).unwrap_err(); + match err { + PolyfillError::Http(msg) => assert!(msg.contains("unsupported URL scheme")), + other => panic!("expected Http error, got: {other}"), + } + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_fetch_sync_rejects_crlf_header_value() { + let req = FetchRequest { + url: "https://example.com".to_string(), + method: "GET".to_string(), + headers: vec![ + ("X-Custom".to_string(), "value\r\nInjected: true".to_string()), + ], + body: None, + }; + let err = fetch_sync(req).unwrap_err(); + match err { + PolyfillError::Http(msg) => assert!(msg.contains("prohibited characters")), + other => panic!("expected Http error for CRLF injection, got: {other}"), + } + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_fetch_sync_rejects_crlf_header_name() { + let req = FetchRequest { + url: "https://example.com".to_string(), + method: "GET".to_string(), + headers: vec![ + ("X-Bad\nName".to_string(), "value".to_string()), + ], + body: None, + }; + let err = fetch_sync(req).unwrap_err(); + match err { + PolyfillError::Http(msg) => assert!(msg.contains("prohibited characters")), + other => panic!("expected Http error for CRLF injection, got: {other}"), + } + } + + #[test] + fn test_random_bytes_hex_length() { + let hex = random_bytes_hex(16).unwrap(); + assert_eq!(hex.len(), 32); + assert!(hex.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_random_bytes_at_max_boundary() { + // Exactly at the limit should succeed + let result = random_bytes(MAX_RANDOM_BYTES); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), MAX_RANDOM_BYTES); + } +} diff --git a/packages/edge/rust/mobile/Cargo.toml b/packages/edge/rust/mobile/Cargo.toml new file mode 100644 index 0000000000..70daf2e225 --- /dev/null +++ b/packages/edge/rust/mobile/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "octobot-edge-mobile" +version = "0.1.0" +edition = "2021" + +[lib] +name = "octobot_edge_mobile" +crate-type = ["staticlib", "cdylib"] + +[dependencies] +octobot-edge-core = { path = "../core" } +uniffi = { version = "0.28", features = ["build"] } +thiserror = { workspace = true } +tokio = { version = "1", features = ["rt"] } + +[build-dependencies] +uniffi = { version = "0.28", features = ["build"] } diff --git a/packages/edge/rust/mobile/build.rs b/packages/edge/rust/mobile/build.rs new file mode 100644 index 0000000000..4f19200fd2 --- /dev/null +++ b/packages/edge/rust/mobile/build.rs @@ -0,0 +1,3 @@ +// The mobile crate uses uniffi::setup_scaffolding!() (proc-macro approach). +// No UDL file is needed — this build.rs is a no-op placeholder required by Cargo. +fn main() {} diff --git a/packages/edge/rust/mobile/src/lib.rs b/packages/edge/rust/mobile/src/lib.rs new file mode 100644 index 0000000000..914d873c03 --- /dev/null +++ b/packages/edge/rust/mobile/src/lib.rs @@ -0,0 +1,151 @@ +// UniFFI wrapper for iOS/Android (React Native Turbo Module). +// uniffi-bindgen-react-native reads these annotations and generates: +// - TypeScript types + JSI C++ glue +// - Swift Turbo Module (iOS) +// - Kotlin Turbo Module (Android) + +uniffi::setup_scaffolding!(); + +use octobot_edge_core as core; + +// ── Error type (re-exported for UniFFI) ────────────────────────────────────── + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum EdgePolyfillError { + #[error("Unsupported algorithm: {0}")] + UnsupportedAlgorithm(String), + #[error("HTTP error: {0}")] + Http(String), + #[error("Encoding error: {0}")] + Encoding(String), +} + +impl From for EdgePolyfillError { + fn from(e: core::PolyfillError) -> Self { + match e { + core::PolyfillError::UnsupportedAlgorithm(s) => Self::UnsupportedAlgorithm(s), + core::PolyfillError::Http(s) => Self::Http(s), + core::PolyfillError::Encoding(s) => Self::Encoding(s), + } + } +} + +// ── Fetch types (exposed to TypeScript) ────────────────────────────────────── + +#[derive(uniffi::Record)] +pub struct FetchRequest { + pub url: String, + pub method: String, + pub headers: Vec, + pub body: Option, +} + +#[derive(uniffi::Record)] +pub struct FetchResponse { + pub status: u16, + pub ok: bool, + pub body: String, + pub headers: Vec, +} + +#[derive(uniffi::Record)] +pub struct HeaderEntry { + pub name: String, + pub value: String, +} + +// ── HMAC ───────────────────────────────────────────────────────────────────── + +/// Compute HMAC. algorithm: "sha256"|"sha512"|"sha384" +/// encoding: "hex"|"base64" +#[uniffi::export] +pub fn hmac( + algorithm: String, + secret: String, + data: String, + encoding: String, +) -> Result { + core::hmac(&algorithm, &secret, &data, &encoding).map_err(Into::into) +} + +// ── Hash ───────────────────────────────────────────────────────────────────── + +/// Compute hash. algorithm: "sha256"|"sha512"|"sha384"|"md5"|"sha3-256"|"sha3-512" +#[uniffi::export] +pub fn hash( + algorithm: String, + data: String, + encoding: String, +) -> Result { + core::hash(&algorithm, &data, &encoding).map_err(Into::into) +} + +// ── Random bytes ───────────────────────────────────────────────────────────── + +#[uniffi::export] +pub fn random_bytes_hex(size: u32) -> Result { + core::random_bytes_hex(size as usize).map_err(Into::into) +} + +#[uniffi::export] +pub fn random_bytes(size: u32) -> Result, EdgePolyfillError> { + core::random_bytes(size as usize).map_err(Into::into) +} + +// ── Buffer ops ─────────────────────────────────────────────────────────────── + +#[uniffi::export] +pub fn buffer_to_hex(data: Vec) -> String { + core::buffer_to_hex(&data) +} + +#[uniffi::export] +pub fn buffer_from_hex(hex_str: String) -> Result, EdgePolyfillError> { + core::buffer_from_hex(&hex_str).map_err(Into::into) +} + +#[uniffi::export] +pub fn buffer_to_base64(data: Vec) -> String { + core::buffer_to_base64(&data) +} + +#[uniffi::export] +pub fn buffer_from_base64(b64: String) -> Result, EdgePolyfillError> { + core::buffer_from_base64(&b64).map_err(Into::into) +} + +#[uniffi::export] +pub fn buffer_byte_length(utf8_str: String) -> u32 { + core::buffer_byte_length(&utf8_str).min(u32::MAX as usize) as u32 +} + +// ── Fetch (async via tokio, mapped to JS Promise by uniffi-bindgen-RN) ─────── + +/// Performs HTTP request from Rust using reqwest + rustls. +/// Mapped to a TypeScript Promise automatically. +#[uniffi::export] +pub async fn fetch(req: FetchRequest) -> Result { + let core_req = core::FetchRequest { + url: req.url, + method: req.method, + headers: req.headers.into_iter().map(|h| (h.name, h.value)).collect(), + body: req.body, + }; + + let result = tokio::task::spawn_blocking(move || { + core::fetch_sync(core_req) + }) + .await + .map_err(|e| EdgePolyfillError::Http(e.to_string()))? + .map_err(EdgePolyfillError::from)?; + + Ok(FetchResponse { + status: result.status, + ok: result.ok, + body: result.body, + headers: result.headers + .into_iter() + .map(|(name, value)| HeaderEntry { name, value }) + .collect(), + }) +} diff --git a/packages/edge/rust/napi/Cargo.toml b/packages/edge/rust/napi/Cargo.toml new file mode 100644 index 0000000000..2c3093a4cb --- /dev/null +++ b/packages/edge/rust/napi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "octobot-edge-napi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +octobot-edge-core = { path = "../core" } +napi = { version = "2", features = ["napi9"] } +napi-derive = "2" +hex = { workspace = true } + +[build-dependencies] +napi-build = "2" diff --git a/packages/edge/rust/napi/build.rs b/packages/edge/rust/napi/build.rs new file mode 100644 index 0000000000..9fc2367889 --- /dev/null +++ b/packages/edge/rust/napi/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/packages/edge/rust/napi/src/lib.rs b/packages/edge/rust/napi/src/lib.rs new file mode 100644 index 0000000000..eea07e2661 --- /dev/null +++ b/packages/edge/rust/napi/src/lib.rs @@ -0,0 +1,81 @@ +use napi_derive::napi; +use octobot_edge_core as core; + +#[napi] +pub fn hmac_sha256(key: Vec, message: Vec) -> napi::Result> { + core::hmac_sha256(&key, &message) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn hmac_sha256_hex(key: Vec, message: Vec) -> napi::Result { + core::hmac_sha256(&key, &message) + .map(hex::encode) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn hmac_sha512(key: Vec, message: Vec) -> napi::Result> { + core::hmac_sha512(&key, &message) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn hmac_sha512_hex(key: Vec, message: Vec) -> napi::Result { + core::hmac_sha512(&key, &message) + .map(hex::encode) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn hmac(algorithm: String, secret: String, data: String, encoding: String) -> napi::Result { + core::hmac(&algorithm, &secret, &data, &encoding) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn hash(algorithm: String, data: String, encoding: String) -> napi::Result { + core::hash(&algorithm, &data, &encoding) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn random_bytes_hex(size: u32) -> napi::Result { + core::random_bytes_hex(size as usize) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn random_bytes(size: u32) -> napi::Result> { + core::random_bytes(size as usize) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +// ── Buffer operations ──────────────────────────────────────────────────────── + +#[napi] +pub fn buffer_to_hex(data: Vec) -> String { + core::buffer_to_hex(&data) +} + +#[napi] +pub fn buffer_from_hex(hex_str: String) -> napi::Result> { + core::buffer_from_hex(&hex_str) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn buffer_to_base64(data: Vec) -> String { + core::buffer_to_base64(&data) +} + +#[napi] +pub fn buffer_from_base64(b64: String) -> napi::Result> { + core::buffer_from_base64(&b64) + .map_err(|e| napi::Error::from_reason(e.to_string())) +} + +#[napi] +pub fn buffer_byte_length(utf8_str: String) -> u32 { + core::buffer_byte_length(&utf8_str).min(u32::MAX as usize) as u32 +} diff --git a/packages/edge/rust/py/Cargo.toml b/packages/edge/rust/py/Cargo.toml new file mode 100644 index 0000000000..842c0fb2d8 --- /dev/null +++ b/packages/edge/rust/py/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "octobot_edge_native" +version = "0.1.0" +edition = "2021" + +[lib] +name = "octobot_edge_native" +crate-type = ["cdylib"] + +[dependencies] +octobot-edge-core = { path = "../core" } +pyo3 = { version = "0.22", features = ["extension-module"] } diff --git a/packages/edge/rust/py/pyproject.toml b/packages/edge/rust/py/pyproject.toml new file mode 100644 index 0000000000..7d513c4e68 --- /dev/null +++ b/packages/edge/rust/py/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["maturin>=1.4"] +build-backend = "maturin" + +[tool.maturin] +python-source = "../../" +module-name = "octobot_edge._native" +manifest-path = "Cargo.toml" diff --git a/packages/edge/rust/py/src/lib.rs b/packages/edge/rust/py/src/lib.rs new file mode 100644 index 0000000000..71f9f99131 --- /dev/null +++ b/packages/edge/rust/py/src/lib.rs @@ -0,0 +1,45 @@ +use pyo3::prelude::*; +use octobot_edge_core as core; + +#[pyfunction] +fn hmac_sha256<'py>(py: Python<'py>, key: &[u8], message: &[u8]) -> PyResult> { + let result = core::hmac_sha256(key, message) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(pyo3::types::PyBytes::new_bound(py, &result)) +} + +#[pyfunction] +fn hmac_sha512<'py>(py: Python<'py>, key: &[u8], message: &[u8]) -> PyResult> { + let result = core::hmac_sha512(key, message) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(pyo3::types::PyBytes::new_bound(py, &result)) +} + +#[pyfunction] +fn hash(algorithm: &str, data: &str, encoding: &str) -> PyResult { + core::hash(algorithm, data, encoding) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) +} + +/// Generic HMAC — matches WASM/NAPI API surface +#[pyfunction] +fn hmac(algorithm: &str, secret: &str, data: &str, encoding: &str) -> PyResult { + core::hmac(algorithm, secret, data, encoding) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) +} + +/// Keep backward compatibility alias +#[pyfunction] +fn hash_digest(algorithm: &str, data: &str, encoding: &str) -> PyResult { + hash(algorithm, data, encoding) +} + +#[pymodule] +fn octobot_edge_native(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(hmac_sha256, m)?)?; + m.add_function(wrap_pyfunction!(hmac_sha512, m)?)?; + m.add_function(wrap_pyfunction!(hmac, m)?)?; + m.add_function(wrap_pyfunction!(hash, m)?)?; + m.add_function(wrap_pyfunction!(hash_digest, m)?)?; + Ok(()) +} diff --git a/packages/edge/rust/wasm/Cargo.toml b/packages/edge/rust/wasm/Cargo.toml new file mode 100644 index 0000000000..eb2aa5db90 --- /dev/null +++ b/packages/edge/rust/wasm/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "octobot-edge-wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +octobot-edge-core = { path = "../core" } +wasm-bindgen = "0.2" +js-sys = "0.3" +hex = { workspace = true } +base64 = { workspace = true } +getrandom = { version = "0.2", features = ["js"] } +web-sys = { version = "0.3", features = ["console"] } diff --git a/packages/edge/rust/wasm/src/lib.rs b/packages/edge/rust/wasm/src/lib.rs new file mode 100644 index 0000000000..0f033ab657 --- /dev/null +++ b/packages/edge/rust/wasm/src/lib.rs @@ -0,0 +1,160 @@ +use wasm_bindgen::prelude::*; +use octobot_edge_core as core; + +/// Compute HMAC — called by JS crypto polyfill +#[wasm_bindgen] +pub fn hmac( + algorithm: &str, + secret: &str, + data: &str, + encoding: &str, +) -> Result { + core::hmac(algorithm, secret, data, encoding) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Compute hash — called by JS crypto polyfill +#[wasm_bindgen] +pub fn hash( + algorithm: &str, + data: &str, + encoding: &str, +) -> Result { + core::hash(algorithm, data, encoding) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// HMAC-SHA256 returning raw bytes +#[wasm_bindgen] +pub fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, JsValue> { + core::hmac_sha256(key, message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// HMAC-SHA256 returning hex string +#[wasm_bindgen] +pub fn hmac_sha256_hex(key: &[u8], message: &[u8]) -> Result { + core::hmac_sha256(key, message) + .map(hex::encode) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// HMAC-SHA512 returning raw bytes +#[wasm_bindgen] +pub fn hmac_sha512(key: &[u8], message: &[u8]) -> Result, JsValue> { + core::hmac_sha512(key, message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// HMAC-SHA512 returning hex string +#[wasm_bindgen] +pub fn hmac_sha512_hex(key: &[u8], message: &[u8]) -> Result { + core::hmac_sha512(key, message) + .map(hex::encode) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Random bytes as hex string +#[wasm_bindgen] +pub fn random_bytes_hex(size: usize) -> Result { + core::random_bytes_hex(size) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Random bytes as Uint8Array +#[wasm_bindgen] +pub fn random_bytes(size: usize) -> Result, JsValue> { + core::random_bytes(size) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Buffer to hex +#[wasm_bindgen] +pub fn buffer_to_hex(data: &[u8]) -> String { + core::buffer_to_hex(data) +} + +/// Buffer from hex +#[wasm_bindgen] +pub fn buffer_from_hex(hex_str: &str) -> Result, JsValue> { + core::buffer_from_hex(hex_str) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Buffer to base64 +#[wasm_bindgen] +pub fn buffer_to_base64(data: &[u8]) -> String { + core::buffer_to_base64(data) +} + +/// Buffer from base64 +#[wasm_bindgen] +pub fn buffer_from_base64(b64: &str) -> Result, JsValue> { + core::buffer_from_base64(b64) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +/// Byte length of UTF-8 string +#[wasm_bindgen] +pub fn buffer_byte_length(s: &str) -> usize { + core::buffer_byte_length(s) +} + +/// Called once on module load — patches globalThis with __rustPolyfills namespace +#[wasm_bindgen(start)] +pub fn install_polyfills() { + if let Err(e) = install_polyfills_inner() { + let msg = e.as_string().unwrap_or_else(|| "unknown error installing polyfills".to_string()); + web_sys::console::error_1(&format!("install_polyfills failed: {msg}").into()); + } +} + +fn install_polyfills_inner() -> Result<(), JsValue> { + let global = js_sys::global(); + let rust_obj = js_sys::Object::new(); + + // hmac(algorithm, secret, data, encoding) -> string (throws on error) + js_sys::Reflect::set(&rust_obj, &"hmac".into(), + &wasm_bindgen::closure::Closure::wrap( + Box::new(|a: String, s: String, d: String, e: String| { + match hmac(&a, &s, &d, &e) { + Ok(result) => result, + Err(err) => { + wasm_bindgen::throw_str(&format!("HMAC error: {}", err.as_string().unwrap_or_default())); + } + } + }) as Box String> + ).into_js_value() + )?; + + // hash(algorithm, data, encoding) -> string (throws on error) + js_sys::Reflect::set(&rust_obj, &"hash".into(), + &wasm_bindgen::closure::Closure::wrap( + Box::new(|a: String, d: String, e: String| { + match hash(&a, &d, &e) { + Ok(result) => result, + Err(err) => { + wasm_bindgen::throw_str(&format!("Hash error: {}", err.as_string().unwrap_or_default())); + } + } + }) as Box String> + ).into_js_value() + )?; + + // randomBytesHex(size) -> string (throws on error) + js_sys::Reflect::set(&rust_obj, &"randomBytesHex".into(), + &wasm_bindgen::closure::Closure::wrap( + Box::new(|size: usize| { + match random_bytes_hex(size) { + Ok(result) => result, + Err(err) => { + wasm_bindgen::throw_str(&format!("randomBytesHex error: {}", err.as_string().unwrap_or_default())); + } + } + }) as Box String> + ).into_js_value() + )?; + + js_sys::Reflect::set(&global, &"__rustPolyfills".into(), &rust_obj)?; + Ok(()) +} diff --git a/packages/edge/tests/__init__.py b/packages/edge/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/edge/tests/static/binance_order.json b/packages/edge/tests/static/binance_order.json new file mode 100644 index 0000000000..b8c82f1e19 --- /dev/null +++ b/packages/edge/tests/static/binance_order.json @@ -0,0 +1,46 @@ +{ + "id": "28457315", + "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + "timestamp": 1700000000000, + "datetime": "2023-11-14T22:13:20.000Z", + "lastTradeTimestamp": null, + "symbol": "ETH/USDT", + "type": "limit", + "timeInForce": "GTC", + "postOnly": false, + "side": "buy", + "price": "2050.00", + "stopPrice": null, + "triggerPrice": null, + "amount": "1.5", + "cost": "1025.00", + "average": "2050.00", + "filled": "0.5", + "remaining": "1.0", + "status": "open", + "fee": { + "cost": "0.00050000", + "currency": "ETH" + }, + "trades": [], + "info": { + "symbol": "ETHUSDT", + "orderId": 28457315, + "orderListId": -1, + "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + "price": "2050.00000000", + "origQty": "1.50000000", + "executedQty": "0.50000000", + "cummulativeQuoteQty": "1025.00000000", + "status": "PARTIALLY_FILLED", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.00000000", + "icebergQty": "0.00000000", + "time": 1700000000000, + "updateTime": 1700000050000, + "isWorking": true, + "origQuoteOrderQty": "0.00000000" + } +} diff --git a/packages/edge/tests/static/binance_ticker.json b/packages/edge/tests/static/binance_ticker.json new file mode 100644 index 0000000000..ab4cd2fabd --- /dev/null +++ b/packages/edge/tests/static/binance_ticker.json @@ -0,0 +1,41 @@ +{ + "symbol": "BTC/USDT", + "timestamp": 1700000000000, + "datetime": "2023-11-14T22:13:20.000Z", + "high": "37500.00", + "low": "36200.00", + "bid": "36800.50", + "bidVolume": "1.234", + "ask": "36801.00", + "askVolume": "0.567", + "vwap": "36850.12", + "open": "36400.00", + "close": "36800.75", + "last": "36800.75", + "previousClose": "36400.00", + "change": "400.75", + "percentage": "1.1", + "average": "36600.375", + "baseVolume": "28543.12", + "quoteVolume": "1051234567.89", + "info": { + "symbol": "BTCUSDT", + "priceChange": "400.75000000", + "priceChangePercent": "1.100", + "weightedAvgPrice": "36850.12000000", + "prevClosePrice": "36400.00000000", + "lastPrice": "36800.75000000", + "lastQty": "0.00100000", + "bidPrice": "36800.50000000", + "bidQty": "1.23400000", + "askPrice": "36801.00000000", + "askQty": "0.56700000", + "openPrice": "36400.00000000", + "highPrice": "37500.00000000", + "lowPrice": "36200.00000000", + "volume": "28543.12000000", + "quoteVolume": "1051234567.89000000", + "openTime": 1699913600000, + "closeTime": 1700000000000 + } +} diff --git a/packages/edge/tests/static/hmac_vectors.json b/packages/edge/tests/static/hmac_vectors.json new file mode 100644 index 0000000000..73eda9d0aa --- /dev/null +++ b/packages/edge/tests/static/hmac_vectors.json @@ -0,0 +1,40 @@ +{ + "description": "HMAC-SHA256/512 test vectors from exchange API documentation", + "vectors": [ + { + "name": "binance_official", + "algorithm": "sha256", + "key": "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j", + "message": "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559", + "expected_hex": "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" + }, + { + "name": "empty_message", + "algorithm": "sha256", + "key": "secret", + "message": "", + "expected_hex": "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169" + }, + { + "name": "empty_key", + "algorithm": "sha256", + "key": "", + "message": "message", + "expected_hex": "eb08c1f56d5ddee07f7bdf80468083da06b64cf4fac64fe3a90883df5feacae4" + }, + { + "name": "sha512_basic", + "algorithm": "sha512", + "key": "secret_key", + "message": "test_message", + "expected_hex": "a2074354902578dd2bbf662b828c5174088eb38105f848f3621fd9ccd3d15cfbabc780d46b4ad8a56615f027fedd273215e98ff1de74d6001e3e919185cb87a7" + }, + { + "name": "bybit_style", + "algorithm": "sha256", + "key": "t7T0YlFnYXk0Fx3JswQsDrViLg1Gh3DUU5Mr", + "message": "1658384314791GET/v5/order/realtime?category=option&symbol=BTC-29JUL22-25000-C", + "expected_hex": "bd874cfc642bdaf817fa7a55ee106f5078b64d51691855a870214658acbb53ce" + } + ] +} diff --git a/packages/edge/tests/test_crypto.py b/packages/edge/tests/test_crypto.py new file mode 100644 index 0000000000..8d222e1b6e --- /dev/null +++ b/packages/edge/tests/test_crypto.py @@ -0,0 +1,104 @@ +"""Tests for octobot_edge.crypto — verifies HMAC correctness with known vectors.""" +import hashlib +import hmac as stdlib_hmac + +import pytest + +from octobot_edge.crypto.hmac import hmac_sha256, hmac_sha512, maybe_rust_hmac + + +class TestHmacSha256: + """Test HMAC-SHA256 against known vectors and stdlib.""" + + BINANCE_SECRET = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + BINANCE_QUERY = ( + "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC" + "&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" + ) + BINANCE_EXPECTED = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" + + def test_binance_signature_vector(self): + """Verify HMAC-SHA256 against the official Binance API docs test vector.""" + result = hmac_sha256( + self.BINANCE_SECRET.encode(), + self.BINANCE_QUERY.encode(), + ) + assert result.hex() == self.BINANCE_EXPECTED + + def test_matches_stdlib(self): + """Verify our HMAC matches Python stdlib for arbitrary inputs.""" + key = b"test-secret-key" + msg = b"test-message-data" + expected = stdlib_hmac.new(key, msg, hashlib.sha256).digest() + assert hmac_sha256(key, msg) == expected + + def test_empty_message(self): + """HMAC of empty message should still produce 32-byte output.""" + result = hmac_sha256(b"key", b"") + assert len(result) == 32 + + def test_empty_key(self): + """HMAC with empty key should still produce valid output.""" + result = hmac_sha256(b"", b"message") + expected = stdlib_hmac.new(b"", b"message", hashlib.sha256).digest() + assert result == expected + + def test_binary_key(self): + """HMAC with binary key (non-UTF-8 bytes).""" + key = bytes(range(256)) + msg = b"binary key test" + expected = stdlib_hmac.new(key, msg, hashlib.sha256).digest() + assert hmac_sha256(key, msg) == expected + + +class TestHmacSha512: + """Test HMAC-SHA512 against known vectors and stdlib.""" + + def test_matches_stdlib(self): + """Verify our HMAC-SHA512 matches Python stdlib.""" + key = b"test-secret-key" + msg = b"test-message-data" + expected = stdlib_hmac.new(key, msg, hashlib.sha512).digest() + assert hmac_sha512(key, msg) == expected + + def test_output_length(self): + """HMAC-SHA512 should produce 64 bytes.""" + result = hmac_sha512(b"key", b"message") + assert len(result) == 64 + + def test_empty_message(self): + """HMAC-SHA512 of empty message should still produce 64-byte output.""" + result = hmac_sha512(b"key", b"") + expected = stdlib_hmac.new(b"key", b"", hashlib.sha512).digest() + assert result == expected + + def test_empty_key(self): + """HMAC-SHA512 with empty key matches stdlib.""" + result = hmac_sha512(b"", b"message") + expected = stdlib_hmac.new(b"", b"message", hashlib.sha512).digest() + assert result == expected + + +class TestMaybeRustHmac: + """Test the maybe_rust_hmac fallback logic.""" + + def test_returns_bytes_or_none_sha256(self): + """Should return bytes (Rust available) or None (fallback).""" + result = maybe_rust_hmac(b"key", b"message") + if result is not None: + assert isinstance(result, bytes) + assert len(result) == 32 + + def test_returns_bytes_or_none_sha512(self): + """SHA-512 variant should return 64 bytes or None.""" + result = maybe_rust_hmac(b"key", b"message", algorithm="sha512") + if result is not None: + assert isinstance(result, bytes) + assert len(result) == 64 + expected = stdlib_hmac.new(b"key", b"message", hashlib.sha512).digest() + assert result == expected + + def test_unsupported_algorithm_returns_none(self): + """Unsupported algorithm should return None (no Rust dispatch).""" + result = maybe_rust_hmac(b"key", b"message", algorithm="sha384") + assert result is None diff --git a/packages/edge/tests/test_e2e.py b/packages/edge/tests/test_e2e.py new file mode 100644 index 0000000000..dd9ee199bd --- /dev/null +++ b/packages/edge/tests/test_e2e.py @@ -0,0 +1,334 @@ +""" +End-to-end tests — verify the full flow from exchange creation through +signing to request assembly, matching ccxt's native behavior exactly. + +Simple tests validate basic crypto and exchange creation. +Advanced tests exercise multi-exchange signing, concurrent usage, +binary/edge-case secrets, and cross-algorithm consistency. +""" +from __future__ import annotations + +import hashlib +import hmac as stdlib_hmac +import secrets +import string +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed + +import pytest +import ccxt + +from octobot_edge import create_exchange, hmac_sha256, hmac_sha512, maybe_rust_hmac + + +# ═══════════════════════════════════════════════════════════════════════════ +# Simple E2E Tests +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestSimpleE2E: + """Simple end-to-end tests: create exchange, sign, verify.""" + + def test_create_and_sign_binance(self): + """Full flow: create Binance exchange, sign a request, verify output.""" + exchange = create_exchange("binance", { + "apiKey": "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A", + "secret": "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j", + }) + + query = b"symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" + secret = b"NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + + result = exchange.hmac(query, secret, "sha256", "hex") + expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" + assert result == expected + + def test_create_and_sign_bybit(self): + """Full flow: create Bybit exchange, sign a request.""" + exchange = create_exchange("bybit", { + "apiKey": "test_api_key", + "secret": "test_secret_key", + }) + result = exchange.hmac(b"test_payload", b"test_secret_key", "sha256", "hex") + expected = stdlib_hmac.new(b"test_secret_key", b"test_payload", hashlib.sha256).hexdigest() + assert result == expected + + def test_direct_hmac_sha256_roundtrip(self): + """Direct hmac_sha256 produces correct bytes that encode to hex properly.""" + key = b"my_secret" + msg = b"timestamp=1700000000000" + result = hmac_sha256(key, msg) + expected = stdlib_hmac.new(key, msg, hashlib.sha256).digest() + assert result == expected + assert result.hex() == expected.hex() + + def test_direct_hmac_sha512_roundtrip(self): + """Direct hmac_sha512 produces correct bytes.""" + key = b"another_secret" + msg = b"order_data" + result = hmac_sha512(key, msg) + expected = stdlib_hmac.new(key, msg, hashlib.sha512).digest() + assert result == expected + + def test_exchange_methods_accessible(self): + """Created exchange exposes all standard ccxt methods.""" + exchange = create_exchange("binance", {}) + for method in ["fetch_ticker", "fetch_order_book", "fetch_ohlcv", + "create_order", "cancel_order", "fetch_balance", + "fetch_trades", "fetch_markets"]: + assert hasattr(exchange, method), f"Missing method: {method}" + + def test_maybe_rust_hmac_returns_bytes_or_none(self): + """maybe_rust_hmac returns bytes for supported algos, None for unsupported.""" + key, msg = b"key", b"msg" + sha256_result = maybe_rust_hmac(key, msg, algorithm="sha256") + sha512_result = maybe_rust_hmac(key, msg, algorithm="sha512") + unsupported = maybe_rust_hmac(key, msg, algorithm="md5") + + if sha256_result is not None: + assert isinstance(sha256_result, bytes) + assert sha256_result == stdlib_hmac.new(key, msg, hashlib.sha256).digest() + if sha512_result is not None: + assert isinstance(sha512_result, bytes) + assert sha512_result == stdlib_hmac.new(key, msg, hashlib.sha512).digest() + assert unsupported is None + + +# ═══════════════════════════════════════════════════════════════════════════ +# Advanced E2E Tests +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestAdvancedE2EMultiExchange: + """Test signing across all major exchanges to verify patching is universal.""" + + EXCHANGES = ["binance", "bybit", "okx", "kucoin", "bitget", "kraken", "huobi", "gate"] + TEST_PAYLOAD = b"symbol=BTCUSDT×tamp=1700000000000&recvWindow=5000" + TEST_SECRET = b"exchange_api_secret_key_12345" + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_each_exchange_hmac_matches_stdlib(self, exchange_id): + """Every supported exchange's patched HMAC must match Python stdlib.""" + try: + exchange = create_exchange(exchange_id, {"apiKey": "k", "secret": "s"}) + except Exception: + pytest.skip(f"{exchange_id} not available in this ccxt version") + + for algo, hashfn in [("sha256", hashlib.sha256), ("sha512", hashlib.sha512)]: + patched = exchange.hmac(self.TEST_PAYLOAD, self.TEST_SECRET, algo, "hex") + expected = stdlib_hmac.new(self.TEST_SECRET, self.TEST_PAYLOAD, hashfn).hexdigest() + assert patched == expected, f"{exchange_id}/{algo}: {patched} != {expected}" + + def test_cross_exchange_signatures_are_identical(self): + """All exchanges with the same input must produce the same signature.""" + results = {} + for eid in self.EXCHANGES: + try: + ex = create_exchange(eid, {"apiKey": "k", "secret": "s"}) + results[eid] = ex.hmac(self.TEST_PAYLOAD, self.TEST_SECRET, "sha256", "hex") + except Exception: + pass + + values = list(results.values()) + assert len(values) >= 2, "Need at least 2 exchanges to compare" + assert len(set(values)) == 1, f"Signatures differ across exchanges: {results}" + + +class TestAdvancedE2EConcurrency: + """Test signing under concurrent access to verify thread safety.""" + + def test_concurrent_signing_is_thread_safe(self): + """Multiple threads signing simultaneously should all produce correct results.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"concurrent_test_secret" + errors = [] + + def sign_and_verify(thread_id: int): + for i in range(100): + msg = f"thread={thread_id}&iter={i}×tamp=1700000000000".encode() + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + if result != expected: + errors.append(f"Thread {thread_id}, iter {i}: {result} != {expected}") + + threads = [threading.Thread(target=sign_and_verify, args=(t,)) for t in range(8)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert len(errors) == 0, f"Thread safety errors:\n" + "\n".join(errors[:10]) + + def test_concurrent_exchange_creation(self): + """Creating and using exchanges concurrently should be safe.""" + results = {} + + def create_and_sign(exchange_id: str): + ex = create_exchange(exchange_id, {"apiKey": "k", "secret": "s"}) + sig = ex.hmac(b"test_data", b"test_secret", "sha256", "hex") + return exchange_id, sig + + with ThreadPoolExecutor(max_workers=4) as pool: + futures = [pool.submit(create_and_sign, eid) for eid in ["binance", "bybit", "okx", "kucoin"]] + for f in as_completed(futures): + eid, sig = f.result() + results[eid] = sig + + expected = stdlib_hmac.new(b"test_secret", b"test_data", hashlib.sha256).hexdigest() + for eid, sig in results.items(): + assert sig == expected, f"{eid} concurrent result mismatch" + + +class TestAdvancedE2EBinarySecrets: + """Test signing with various binary secret formats (base64-decoded, raw bytes).""" + + def test_base64_decoded_secret(self): + """Exchanges like Coinbase use base64-encoded secrets.""" + import base64 + raw_secret = secrets.token_bytes(32) + b64_secret = base64.b64encode(raw_secret) + + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac(b"test", raw_secret, "sha256", "hex") + expected = stdlib_hmac.new(raw_secret, b"test", hashlib.sha256).hexdigest() + assert result == expected + + def test_secret_with_all_byte_values(self): + """Secret containing every possible byte value (0x00–0xFF).""" + secret = bytes(range(256)) + message = b"sign_this" + + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac(message, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, message, hashlib.sha256).hexdigest() + assert result == expected + + def test_empty_inputs(self): + """Empty message and/or empty secret should still produce valid HMAC.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + + for msg, secret in [(b"", b"key"), (b"msg", b""), (b"", b"")]: + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected, f"Empty input mismatch: msg={msg!r}, secret={secret!r}" + + def test_very_large_payload(self): + """Large payloads (1MB) should sign correctly without truncation.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"x" * (1024 * 1024) + secret = b"secret" + + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_unicode_query_string(self): + """Non-ASCII characters in query strings.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = "symbol=BTC/USDT¬e=日本語テスト&price=1000€".encode("utf-8") + secret = b"secret" + + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + +class TestAdvancedE2EAlgorithmConsistency: + """Verify cross-algorithm consistency and fallback behavior.""" + + def test_sha256_and_sha512_differ(self): + """Same input with different algorithms must produce different signatures.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg, secret = b"test", b"secret" + + sha256 = exchange.hmac(msg, secret, "sha256", "hex") + sha512 = exchange.hmac(msg, secret, "sha512", "hex") + + assert sha256 != sha512 + assert len(sha256) == 64 # SHA-256 = 32 bytes = 64 hex chars + assert len(sha512) == 128 # SHA-512 = 64 bytes = 128 hex chars + + def test_direct_and_exchange_hmac_agree(self): + """Direct hmac_sha256/sha512 and exchange.hmac produce identical results.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg, secret = b"payload_data", b"api_secret" + + direct_256 = hmac_sha256(secret, msg).hex() + exchange_256 = exchange.hmac(msg, secret, "sha256", "hex") + assert direct_256 == exchange_256 + + direct_512 = hmac_sha512(secret, msg).hex() + exchange_512 = exchange.hmac(msg, secret, "sha512", "hex") + assert direct_512 == exchange_512 + + def test_sync_and_async_exchange_agree(self): + """Sync and async exchanges must produce identical signatures.""" + sync_ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + async_ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}, async_mode=True) + + msg, secret = b"timestamp=1700000000000", b"secret" + assert sync_ex.hmac(msg, secret, "sha256", "hex") == async_ex.hmac(msg, secret, "sha256", "hex") + assert sync_ex.hmac(msg, secret, "sha512", "hex") == async_ex.hmac(msg, secret, "sha512", "hex") + + def test_many_random_inputs(self): + """Fuzz: 500 random payloads must all match stdlib.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + + for _ in range(500): + msg = secrets.token_bytes(secrets.randbelow(512) + 1) + secret = secrets.token_bytes(secrets.randbelow(128) + 1) + + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + +class TestAdvancedE2ESigningFlow: + """Test the full request-signing flow as exchanges do it internally.""" + + def test_binance_order_signing_flow(self): + """Simulate the full Binance order signing flow.""" + api_key = "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A" + api_secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + + exchange = create_exchange("binance", { + "apiKey": api_key, + "secret": api_secret, + }) + + # Build query string as Binance SDK does + params = { + "symbol": "LTCBTC", + "side": "BUY", + "type": "LIMIT", + "timeInForce": "GTC", + "quantity": "1", + "price": "0.1", + "recvWindow": "5000", + "timestamp": "1499827319559", + } + query_string = "&".join(f"{k}={v}" for k, v in params.items()) + signature = exchange.hmac( + query_string.encode(), api_secret.encode(), "sha256", "hex" + ) + + # Verify against Binance's documented expected signature + assert signature == "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" + + # The signed URL would be: query_string + &signature= + signed_url = f"{query_string}&signature={signature}" + assert "signature=c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" in signed_url + + def test_rapid_successive_different_signatures(self): + """Rapidly signing different messages should never cross-contaminate.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"secret" + + prev = None + for i in range(1000): + msg = f"nonce={i}".encode() + sig = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert sig == expected + assert sig != prev, f"Signatures should differ: iter {i}" + prev = sig diff --git a/packages/edge/tests/test_e2e_advanced.py b/packages/edge/tests/test_e2e_advanced.py new file mode 100644 index 0000000000..4bedd23b3c --- /dev/null +++ b/packages/edge/tests/test_e2e_advanced.py @@ -0,0 +1,654 @@ +""" +Advanced end-to-end tests — stress, concurrency, encoding variants, +cross-exchange consistency, fuzz, error boundaries, and full signing flows. + +These complement the basic test_e2e.py with deeper coverage. +""" +from __future__ import annotations + +import asyncio +import base64 +import hashlib +import hmac as stdlib_hmac +import json +import os +import secrets +import string +import struct +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import pytest +import ccxt +import ccxt.async_support as ccxt_async + +from octobot_edge import create_exchange, hmac_sha256, hmac_sha512, maybe_rust_hmac + + +# ═══════════════════════════════════════════════════════════════════════════ +# Cross-Exchange Signing Consistency +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestCrossExchangeConsistency: + """Verify all exchanges produce identical signatures for identical inputs.""" + + EXCHANGES = ["binance", "bybit", "okx", "kucoin", "bitget", "kraken", "huobi", "gate"] + PAYLOAD = b"symbol=BTCUSDT×tamp=1700000000000&recvWindow=5000" + SECRET = b"exchange_api_secret_key_12345" + + def test_sha256_consistency_across_all_exchanges(self): + """SHA-256 hex signatures must be identical across all exchanges.""" + expected = stdlib_hmac.new(self.SECRET, self.PAYLOAD, hashlib.sha256).hexdigest() + results = {} + for eid in self.EXCHANGES: + try: + ex = create_exchange(eid, {"apiKey": "k", "secret": "s"}) + results[eid] = ex.hmac(self.PAYLOAD, self.SECRET, "sha256", "hex") + except Exception: + pass + assert len(results) >= 4, f"Need at least 4 exchanges, got: {list(results.keys())}" + for eid, sig in results.items(): + assert sig == expected, f"{eid}: {sig} != {expected}" + + def test_sha512_consistency_across_all_exchanges(self): + """SHA-512 hex signatures must be identical across all exchanges.""" + expected = stdlib_hmac.new(self.SECRET, self.PAYLOAD, hashlib.sha512).hexdigest() + results = {} + for eid in self.EXCHANGES: + try: + ex = create_exchange(eid, {"apiKey": "k", "secret": "s"}) + results[eid] = ex.hmac(self.PAYLOAD, self.SECRET, "sha512", "hex") + except Exception: + pass + assert len(results) >= 4 + for eid, sig in results.items(): + assert sig == expected, f"{eid}: {sig} != {expected}" + + def test_base64_encoding_consistency_across_exchanges(self): + """base64-encoded HMAC should be consistent across exchanges.""" + expected = base64.b64encode( + stdlib_hmac.new(self.SECRET, self.PAYLOAD, hashlib.sha256).digest() + ).decode("ascii") + results = {} + for eid in self.EXCHANGES: + try: + ex = create_exchange(eid, {"apiKey": "k", "secret": "s"}) + results[eid] = ex.hmac(self.PAYLOAD, self.SECRET, "sha256", "base64") + except Exception: + pass + assert len(results) >= 4 + for eid, sig in results.items(): + assert sig == expected, f"{eid}: {sig} != {expected}" + + +# ═══════════════════════════════════════════════════════════════════════════ +# Digest Encoding Variants +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestDigestEncodingVariants: + """Test hex, base64, and raw byte output formats.""" + + def test_hex_output(self): + ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = ex.hmac(b"data", b"secret", "sha256", "hex") + expected = stdlib_hmac.new(b"secret", b"data", hashlib.sha256).hexdigest() + assert result == expected + assert all(c in "0123456789abcdef" for c in result) + + def test_base64_output(self): + ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = ex.hmac(b"data", b"secret", "sha256", "base64") + expected = base64.b64encode( + stdlib_hmac.new(b"secret", b"data", hashlib.sha256).digest() + ).decode("ascii") + assert result == expected + + def test_hex_and_base64_decode_to_same_bytes(self): + """hex and base64 are just encodings of the same underlying bytes.""" + ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + hex_result = ex.hmac(b"msg", b"key", "sha256", "hex") + b64_result = ex.hmac(b"msg", b"key", "sha256", "base64") + + hex_bytes = bytes.fromhex(hex_result) + b64_bytes = base64.b64decode(b64_result) + assert hex_bytes == b64_bytes + + def test_sha512_base64_output(self): + ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = ex.hmac(b"data", b"secret", "sha512", "base64") + expected = base64.b64encode( + stdlib_hmac.new(b"secret", b"data", hashlib.sha512).digest() + ).decode("ascii") + assert result == expected + + +# ═══════════════════════════════════════════════════════════════════════════ +# Advanced Concurrency Tests +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestAdvancedConcurrency: + """Stress test concurrent access patterns.""" + + def test_16_threads_x_500_iterations(self): + """Heavy concurrent signing load.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"heavy_load_secret" + errors = [] + + def sign_loop(thread_id: int): + for i in range(500): + msg = f"t={thread_id}&i={i}&ts=1700000000000".encode() + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + if result != expected: + errors.append(f"Thread {thread_id} iter {i}") + + threads = [threading.Thread(target=sign_loop, args=(t,)) for t in range(16)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert len(errors) == 0, f"{len(errors)} thread safety errors" + + def test_concurrent_multi_exchange_signing(self): + """Different exchanges signing concurrently on different threads.""" + exchange_ids = ["binance", "bybit", "okx", "kucoin"] + errors = [] + + def sign_on_exchange(eid: str): + try: + ex = create_exchange(eid, {"apiKey": "k", "secret": "s"}) + for i in range(200): + msg = f"exchange={eid}&nonce={i}".encode() + secret = b"multi_exchange_secret" + result = ex.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + if result != expected: + errors.append(f"{eid} iter {i}") + except Exception as e: + errors.append(f"{eid} creation failed: {e}") + + with ThreadPoolExecutor(max_workers=4) as pool: + futures = [pool.submit(sign_on_exchange, eid) for eid in exchange_ids] + for f in as_completed(futures): + f.result() # raises if thread had unhandled exception + + assert len(errors) == 0, f"Concurrent multi-exchange errors: {errors[:10]}" + + def test_alternating_sha256_sha512_under_load(self): + """Rapidly alternating between SHA-256 and SHA-512 under load.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"alternating_algo_secret" + errors = [] + + def alternate(thread_id: int): + for i in range(300): + msg = f"alt={thread_id}&i={i}".encode() + algo = "sha256" if i % 2 == 0 else "sha512" + hashfn = hashlib.sha256 if algo == "sha256" else hashlib.sha512 + result = exchange.hmac(msg, secret, algo, "hex") + expected = stdlib_hmac.new(secret, msg, hashfn).hexdigest() + if result != expected: + errors.append(f"Thread {thread_id} iter {i} algo {algo}") + + threads = [threading.Thread(target=alternate, args=(t,)) for t in range(8)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert len(errors) == 0, f"Alternating algo errors: {errors[:10]}" + + +# ═══════════════════════════════════════════════════════════════════════════ +# Async Exchange Tests +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestAsyncExchange: + """Test async exchange variant matches sync behavior exactly.""" + + def test_async_sync_parity_all_algos(self): + """Async and sync exchanges must produce identical signatures.""" + sync_ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + async_ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}, async_mode=True) + + for algo, hashfn in [("sha256", hashlib.sha256), ("sha512", hashlib.sha512)]: + for i in range(50): + msg = f"nonce={i}".encode() + secret = b"async_test_secret" + sync_result = sync_ex.hmac(msg, secret, algo, "hex") + async_result = async_ex.hmac(msg, secret, algo, "hex") + expected = stdlib_hmac.new(secret, msg, hashfn).hexdigest() + assert sync_result == async_result == expected + + def test_async_base64_encoding(self): + async_ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}, async_mode=True) + result = async_ex.hmac(b"test", b"secret", "sha256", "base64") + expected = base64.b64encode( + stdlib_hmac.new(b"secret", b"test", hashlib.sha256).digest() + ).decode("ascii") + assert result == expected + + @pytest.mark.parametrize("exchange_id", ["binance", "bybit", "okx", "kucoin"]) + def test_async_exchange_methods_available(self, exchange_id): + """Async exchange should have all standard async methods.""" + try: + ex = create_exchange(exchange_id, {}, async_mode=True) + except Exception: + pytest.skip(f"{exchange_id} async not available") + for method in ["fetch_ticker", "fetch_order_book", "create_order", "fetch_balance"]: + assert hasattr(ex, method), f"{exchange_id} missing {method}" + + +# ═══════════════════════════════════════════════════════════════════════════ +# Fuzz Testing +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestFuzzing: + """Fuzz with random inputs to catch edge cases.""" + + def test_1000_random_hmac_sha256(self): + """1000 random payloads, all must match stdlib.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + for _ in range(1000): + msg = secrets.token_bytes(secrets.randbelow(512) + 1) + secret = secrets.token_bytes(secrets.randbelow(128) + 1) + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_500_random_hmac_sha512(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + for _ in range(500): + msg = secrets.token_bytes(secrets.randbelow(256) + 1) + secret = secrets.token_bytes(secrets.randbelow(64) + 1) + result = exchange.hmac(msg, secret, "sha512", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha512).hexdigest() + assert result == expected + + def test_500_random_base64_encoding(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + for _ in range(500): + msg = secrets.token_bytes(secrets.randbelow(256) + 1) + secret = secrets.token_bytes(secrets.randbelow(64) + 1) + result = exchange.hmac(msg, secret, "sha256", "base64") + expected = base64.b64encode( + stdlib_hmac.new(secret, msg, hashlib.sha256).digest() + ).decode("ascii") + assert result == expected + + def test_direct_hmac_sha256_fuzz(self): + """Fuzz the direct hmac_sha256 function.""" + for _ in range(500): + key = secrets.token_bytes(secrets.randbelow(128) + 1) + msg = secrets.token_bytes(secrets.randbelow(512) + 1) + result = hmac_sha256(key, msg) + expected = stdlib_hmac.new(key, msg, hashlib.sha256).digest() + assert result == expected + + def test_direct_hmac_sha512_fuzz(self): + for _ in range(500): + key = secrets.token_bytes(secrets.randbelow(128) + 1) + msg = secrets.token_bytes(secrets.randbelow(512) + 1) + result = hmac_sha512(key, msg) + expected = stdlib_hmac.new(key, msg, hashlib.sha512).digest() + assert result == expected + + +# ═══════════════════════════════════════════════════════════════════════════ +# Exchange Signing Flow Simulations +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestExchangeSigningFlows: + """Simulate real exchange API signing protocols.""" + + def test_binance_spot_market_order(self): + """Full Binance spot market order signing flow.""" + api_secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + exchange = create_exchange("binance", {"apiKey": "test", "secret": api_secret}) + + params = { + "symbol": "BTCUSDT", + "side": "BUY", + "type": "MARKET", + "quoteOrderQty": "100", + "newOrderRespType": "FULL", + "recvWindow": "5000", + "timestamp": "1700000000000", + } + query = "&".join(f"{k}={v}" for k, v in params.items()).encode() + sig = exchange.hmac(query, api_secret.encode(), "sha256", "hex") + expected = stdlib_hmac.new(api_secret.encode(), query, hashlib.sha256).hexdigest() + assert sig == expected + + def test_bybit_v5_signing_flow(self): + """Simulate Bybit v5 API signing: timestamp + apiKey + recvWindow + queryString.""" + api_key = "BYBIT_API_KEY" + api_secret = "BYBIT_SECRET_KEY" + timestamp = "1700000000000" + recv_window = "5000" + query_string = "category=linear&symbol=BTCUSDT" + + sign_payload = f"{timestamp}{api_key}{recv_window}{query_string}".encode() + exchange = create_exchange("bybit", {"apiKey": api_key, "secret": api_secret}) + sig = exchange.hmac(sign_payload, api_secret.encode(), "sha256", "hex") + expected = stdlib_hmac.new(api_secret.encode(), sign_payload, hashlib.sha256).hexdigest() + assert sig == expected + + def test_okx_signing_flow_base64(self): + """Simulate OKX API signing: timestamp + method + path + body, base64 encoded.""" + secret_key = "OKX_SECRET_KEY" + timestamp = "2023-11-15T10:00:00.000Z" + method = "GET" + path = "/api/v5/account/balance" + body = "" + + pre_sign = f"{timestamp}{method}{path}{body}".encode() + exchange = create_exchange("okx", {"apiKey": "k", "secret": secret_key}) + sig = exchange.hmac(pre_sign, secret_key.encode(), "sha256", "base64") + expected = base64.b64encode( + stdlib_hmac.new(secret_key.encode(), pre_sign, hashlib.sha256).digest() + ).decode("ascii") + assert sig == expected + + def test_okx_post_with_json_body(self): + """OKX POST request signing includes JSON body.""" + secret_key = "OKX_SECRET_KEY" + timestamp = "2023-11-15T10:00:00.000Z" + method = "POST" + path = "/api/v5/trade/order" + body = json.dumps({ + "instId": "BTC-USDT", + "tdMode": "cash", + "side": "buy", + "ordType": "limit", + "px": "50000", + "sz": "0.001", + }) + + pre_sign = f"{timestamp}{method}{path}{body}".encode() + exchange = create_exchange("okx", {"apiKey": "k", "secret": secret_key}) + sig = exchange.hmac(pre_sign, secret_key.encode(), "sha256", "base64") + expected = base64.b64encode( + stdlib_hmac.new(secret_key.encode(), pre_sign, hashlib.sha256).digest() + ).decode("ascii") + assert sig == expected + + def test_kucoin_signing_flow(self): + """Simulate KuCoin API signing: timestamp + method + endpoint + body.""" + api_secret = "KUCOIN_SECRET" + timestamp = "1700000000000" + method = "GET" + endpoint = "/api/v1/accounts" + + sign_str = f"{timestamp}{method}{endpoint}".encode() + exchange = create_exchange("kucoin", {"apiKey": "k", "secret": api_secret}) + sig = exchange.hmac(sign_str, api_secret.encode(), "sha256", "base64") + expected = base64.b64encode( + stdlib_hmac.new(api_secret.encode(), sign_str, hashlib.sha256).digest() + ).decode("ascii") + assert sig == expected + + def test_timestamp_rotation_signing(self): + """Simulate rapid order placement with rotating timestamps.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"rapid_order_secret" + base_ts = 1700000000000 + sigs = set() + + for i in range(500): + ts = base_ts + i + msg = f"symbol=BTCUSDT&side=BUY&quantity=0.001×tamp={ts}".encode() + sig = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert sig == expected + sigs.add(sig) + + assert len(sigs) == 500, "All timestamps should produce unique signatures" + + +# ═══════════════════════════════════════════════════════════════════════════ +# Edge Cases & Error Boundaries +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestEdgeCasesAdvanced: + """Deep edge case coverage.""" + + def test_null_bytes_in_message(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"before\x00after\x00end" + secret = b"null_byte_secret" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_null_bytes_in_secret(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"test_message" + secret = b"sec\x00ret\x00key" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_newlines_and_control_chars(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"line1\nline2\rline3\tline4\x00line5" + secret = b"ctrl_secret" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_very_long_secret_exceeding_block_size(self): + """HMAC internally hashes secrets longer than the hash block size.""" + # SHA-256 block size is 64 bytes; secrets > 64 bytes are hashed first + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + for length in [63, 64, 65, 128, 256, 1024, 4096]: + secret = secrets.token_bytes(length) + msg = b"test_payload" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected, f"Failed at secret length {length}" + + def test_10mb_payload(self): + """Very large payload (10MB).""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"x" * (10 * 1024 * 1024) + secret = b"large_payload_secret" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_all_byte_values_in_payload(self): + """Message containing every possible byte value.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = bytes(range(256)) * 4 # 1024 bytes with all byte values + secret = b"all_bytes_secret" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_unicode_query_strings(self): + """Non-ASCII characters in query strings (UTF-8 encoded).""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + test_cases = [ + "symbol=BTC/USDT¬e=日本語テスト", + "name=Ünîcödé&emoji=🌍🚀", + "data=café&price=1000€", + "cyrillic=Привет&arabic=مرحبا", + ] + for msg_str in test_cases: + msg = msg_str.encode("utf-8") + secret = b"unicode_secret" + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected, f"Failed for: {msg_str}" + + def test_json_body_signing(self): + """Signing JSON body payloads (common in REST APIs).""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + bodies = [ + {"symbol": "BTCUSDT", "quantity": 0.001, "price": 50000.00}, + {"pairs": ["BTC/USDT", "ETH/USDT"], "nested": {"key": "value"}}, + {}, # empty object + {"unicode": "日本語", "special": "a&b=c"}, + ] + secret = b"json_secret" + for body in bodies: + msg = json.dumps(body, separators=(",", ":")).encode() + result = exchange.hmac(msg, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + assert result == expected + + def test_encoded_string_inputs(self): + """Pre-encoded string inputs produce correct results.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg_str = "timestamp=1700000000000" + secret_str = "my_api_secret" + + # The standard ccxt path expects bytes; verify encoding is consistent + result = exchange.hmac(msg_str.encode(), secret_str.encode(), "sha256", "hex") + expected = stdlib_hmac.new(secret_str.encode(), msg_str.encode(), hashlib.sha256).hexdigest() + assert result == expected + + # Verify different encodings of the same logical string match + msg_utf8 = msg_str.encode("utf-8") + msg_ascii = msg_str.encode("ascii") + result_utf8 = exchange.hmac(msg_utf8, secret_str.encode(), "sha256", "hex") + result_ascii = exchange.hmac(msg_ascii, secret_str.encode(), "sha256", "hex") + assert result_utf8 == result_ascii == expected + + +# ═══════════════════════════════════════════════════════════════════════════ +# Signing Determinism +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestSigningDeterminism: + """Verify deterministic output under various conditions.""" + + def test_1000_identical_calls(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"symbol=BTCUSDT×tamp=1700000000000" + secret = b"determinism_secret" + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + + for _ in range(1000): + assert exchange.hmac(msg, secret, "sha256", "hex") == expected + + def test_determinism_across_exchange_instances(self): + """Multiple instances of same exchange must produce identical results.""" + msg = b"shared_payload" + secret = b"shared_secret" + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + + for _ in range(100): + ex = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + assert ex.hmac(msg, secret, "sha256", "hex") == expected + + def test_determinism_across_different_exchanges(self): + """Different exchange types produce identical HMAC for same inputs.""" + msg = b"cross_exchange_payload" + secret = b"cross_exchange_secret" + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + + for eid in ["binance", "bybit", "okx", "kucoin"]: + try: + ex = create_exchange(eid, {"apiKey": "k", "secret": "s"}) + assert ex.hmac(msg, secret, "sha256", "hex") == expected + except Exception: + pass + + def test_direct_function_determinism(self): + """Direct hmac functions are deterministic.""" + key = b"deterministic_key" + msg = b"deterministic_message" + expected_256 = stdlib_hmac.new(key, msg, hashlib.sha256).digest() + expected_512 = stdlib_hmac.new(key, msg, hashlib.sha512).digest() + + for _ in range(500): + assert hmac_sha256(key, msg) == expected_256 + assert hmac_sha512(key, msg) == expected_512 + + +# ═══════════════════════════════════════════════════════════════════════════ +# Algorithm Edge Cases +# ═══════════════════════════════════════════════════════════════════════════ + + +class TestAlgorithmEdgeCases: + """Test algorithm handling, fallback, and edge cases.""" + + def test_hashlib_module_as_algorithm(self): + """ccxt sometimes passes hashlib.sha256 directly as algorithm.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"test" + secret = b"secret" + + # String algorithm + str_result = exchange.hmac(msg, secret, "sha256", "hex") + # hashlib module as algorithm + mod_result = exchange.hmac(msg, secret, hashlib.sha256, "hex") + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + + assert str_result == expected + assert mod_result == expected + + def test_case_insensitive_algorithm(self): + """Algorithm names should be case-insensitive.""" + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + msg = b"test" + secret = b"secret" + expected = stdlib_hmac.new(secret, msg, hashlib.sha256).hexdigest() + + for algo in ["sha256", "SHA256", "Sha256"]: + result = exchange.hmac(msg, secret, algo, "hex") + assert result == expected, f"Failed for algorithm: {algo}" + + def test_none_algorithm_defaults_to_sha256(self): + """None algorithm defaults to sha256 in _normalize_algorithm.""" + from octobot_edge.exchange.client import _normalize_algorithm + assert _normalize_algorithm(None) == "sha256" + assert _normalize_algorithm(hashlib.sha256) == "sha256" + assert _normalize_algorithm(hashlib.sha512) == "sha512" + assert _normalize_algorithm("SHA256") == "sha256" + + def test_sha256_output_length(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac(b"m", b"k", "sha256", "hex") + assert len(result) == 64 # 32 bytes = 64 hex chars + + def test_sha512_output_length(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac(b"m", b"k", "sha512", "hex") + assert len(result) == 128 # 64 bytes = 128 hex chars + + def test_maybe_rust_hmac_all_inputs(self): + """Comprehensive test of maybe_rust_hmac behavior.""" + key = b"test_key" + msg = b"test_msg" + + r256 = maybe_rust_hmac(key, msg, algorithm="sha256") + r512 = maybe_rust_hmac(key, msg, algorithm="sha512") + rmd5 = maybe_rust_hmac(key, msg, algorithm="md5") + + # sha256 and sha512 should return bytes (or None if no Rust) + if r256 is not None: + assert isinstance(r256, bytes) + assert len(r256) == 32 + assert r256 == stdlib_hmac.new(key, msg, hashlib.sha256).digest() + if r512 is not None: + assert isinstance(r512, bytes) + assert len(r512) == 64 + assert r512 == stdlib_hmac.new(key, msg, hashlib.sha512).digest() + + # md5 is unsupported, should return None + assert rmd5 is None diff --git a/packages/edge/tests/test_exchange.py b/packages/edge/tests/test_exchange.py new file mode 100644 index 0000000000..2b8d9826b2 --- /dev/null +++ b/packages/edge/tests/test_exchange.py @@ -0,0 +1,125 @@ +"""Tests for octobot_edge.exchange — verifies exchange factory and patching.""" +from unittest.mock import MagicMock, patch + +import pytest + +from octobot_edge.exchange.client import create_exchange, _patch_signing +from octobot_edge.exchange.normalize import normalize_ticker, normalize_order + + +class TestCreateExchange: + """Test the exchange factory.""" + + def test_creates_sync_exchange(self): + """create_exchange should return a ccxt exchange instance.""" + exchange = create_exchange("binance", {}) + import ccxt + assert isinstance(exchange, ccxt.binance) + + def test_creates_async_exchange(self): + """create_exchange with async_mode=True should return async exchange.""" + exchange = create_exchange("binance", {}, async_mode=True) + import ccxt.async_support as ccxt_async + assert isinstance(exchange, ccxt_async.binance) + + def test_passes_config(self): + """Config dict should be forwarded to ccxt constructor.""" + exchange = create_exchange("binance", {"timeout": 5000}) + assert exchange.timeout == 5000 + + def test_unknown_exchange_raises(self): + """Requesting a non-existent exchange should raise AttributeError.""" + with pytest.raises(AttributeError): + create_exchange("nonexistent_exchange_xyz", {}) + + +class TestPatchSigning: + """Test the HMAC patching logic.""" + + def test_patch_overrides_hmac(self): + """After patching, exchange.hmac should be our custom function.""" + exchange = MagicMock() + exchange.hmac = MagicMock(return_value="original") + _patch_signing(exchange) + # The hmac attribute should now be our fast_hmac wrapper + assert callable(exchange.hmac) + assert exchange.hmac.__name__ == "fast_hmac" + + def test_patch_falls_back_for_non_sha256(self): + """For algorithms other than sha256, should fall back to original.""" + exchange = MagicMock() + original_hmac = MagicMock(return_value="original_result") + exchange.hmac = original_hmac + _patch_signing(exchange) + result = exchange.hmac("request", "secret", "sha384", "hex") + original_hmac.assert_called_once_with("request", "secret", "sha384", "hex") + assert result == "original_result" + + +class TestNormalizeTicker: + """Test ticker normalization.""" + + def test_normalizes_complete_ticker(self): + raw = { + "symbol": "BTC/USDT", + "last": "50000.0", + "bid": "49999.0", + "ask": "50001.0", + "high": "51000.0", + "low": "49000.0", + "baseVolume": "1234.5", + "timestamp": 1700000000000, + } + result = normalize_ticker(raw) + assert result["symbol"] == "BTC/USDT" + assert result["last"] == 50000.0 + assert result["bid"] == 49999.0 + assert result["ask"] == 50001.0 + assert result["volume"] == 1234.5 + assert result["timestamp"] == 1700000000000 + + def test_handles_missing_fields(self): + result = normalize_ticker({}) + assert result["symbol"] is None + assert result["last"] is None + assert result["volume"] is None + + def test_handles_non_numeric(self): + result = normalize_ticker({"last": "invalid"}) + assert result["last"] is None + + +class TestNormalizeOrder: + """Test order normalization.""" + + def test_normalizes_complete_order(self): + raw = { + "id": "12345", + "symbol": "ETH/USDT", + "side": "buy", + "type": "limit", + "price": "3000.0", + "amount": "1.5", + "filled": "0.5", + "remaining": "1.0", + "status": "open", + "timestamp": 1700000000000, + } + result = normalize_order(raw) + assert result["id"] == "12345" + assert result["symbol"] == "ETH/USDT" + assert result["side"] == "buy" + assert result["type"] == "limit" + assert result["price"] == 3000.0 + assert result["amount"] == 1.5 + assert result["filled"] == 0.5 + assert result["remaining"] == 1.0 + assert result["status"] == "open" + assert result["timestamp"] == 1700000000000 + + def test_handles_missing_fields(self): + result = normalize_order({}) + assert result["id"] is None + assert result["symbol"] is None + assert result["price"] is None + assert result["remaining"] is None diff --git a/packages/edge/tests/test_integration.py b/packages/edge/tests/test_integration.py new file mode 100644 index 0000000000..c28065cd9a --- /dev/null +++ b/packages/edge/tests/test_integration.py @@ -0,0 +1,344 @@ +"""Integration tests — real ccxt exchange instances with patched signing.""" +from __future__ import annotations + +import hashlib +import hmac as stdlib_hmac +import json +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +import ccxt +import ccxt.async_support as ccxt_async + +from octobot_edge.exchange.client import create_exchange, _patch_signing +from octobot_edge.exchange.normalize import normalize_ticker, normalize_order +from octobot_edge.crypto.hmac import hmac_sha256, maybe_rust_hmac + +STATIC = Path(__file__).parent / "static" + + +# ── Fixture-driven HMAC tests ────────────────────────────────────────────── + +class TestHmacVectors: + """Validate HMAC against exchange-documented test vectors from static fixtures.""" + + @pytest.fixture(scope="class") + def vectors(self): + with open(STATIC / "hmac_vectors.json") as f: + return json.load(f)["vectors"] + + def test_all_sha256_vectors(self, vectors): + for v in vectors: + if v["algorithm"] != "sha256": + continue + result = hmac_sha256(v["key"].encode(), v["message"].encode()) + assert result.hex() == v["expected_hex"], ( + f"HMAC-SHA256 mismatch for vector '{v['name']}': " + f"got {result.hex()}, expected {v['expected_hex']}" + ) + + def test_all_sha512_vectors(self, vectors): + for v in vectors: + if v["algorithm"] != "sha512": + continue + expected = stdlib_hmac.new( + v["key"].encode(), v["message"].encode(), hashlib.sha512 + ).digest() + assert expected.hex() == v["expected_hex"], ( + f"HMAC-SHA512 stdlib mismatch for vector '{v['name']}'" + ) + + def test_binance_vector_matches_fixture(self, vectors): + binance = next(v for v in vectors if v["name"] == "binance_official") + result = hmac_sha256(binance["key"].encode(), binance["message"].encode()) + assert result.hex() == binance["expected_hex"] + + +# ── Fixture-driven normalization tests ────────────────────────────────────── + +class TestNormalizeFromFixtures: + """Normalize real exchange API responses from static JSON fixtures.""" + + @pytest.fixture(scope="class") + def raw_ticker(self): + with open(STATIC / "binance_ticker.json") as f: + return json.load(f) + + @pytest.fixture(scope="class") + def raw_order(self): + with open(STATIC / "binance_order.json") as f: + return json.load(f) + + def test_ticker_all_fields_present(self, raw_ticker): + result = normalize_ticker(raw_ticker) + expected_keys = {"symbol", "last", "bid", "ask", "high", "low", "volume", "timestamp"} + assert set(result.keys()) == expected_keys + + def test_ticker_numeric_conversion(self, raw_ticker): + result = normalize_ticker(raw_ticker) + assert result["last"] == 36800.75 + assert result["bid"] == 36800.50 + assert result["ask"] == 36801.00 + assert result["high"] == 37500.00 + assert result["low"] == 36200.00 + assert result["volume"] == 28543.12 + + def test_ticker_preserves_non_numeric(self, raw_ticker): + result = normalize_ticker(raw_ticker) + assert result["symbol"] == "BTC/USDT" + assert result["timestamp"] == 1700000000000 + + def test_order_all_fields_present(self, raw_order): + result = normalize_order(raw_order) + expected_keys = { + "id", "symbol", "side", "type", "price", + "amount", "filled", "remaining", "status", "timestamp", + } + assert set(result.keys()) == expected_keys + + def test_order_numeric_conversion(self, raw_order): + result = normalize_order(raw_order) + assert result["price"] == 2050.00 + assert result["amount"] == 1.5 + assert result["filled"] == 0.5 + assert result["remaining"] == 1.0 + + def test_order_preserves_strings(self, raw_order): + result = normalize_order(raw_order) + assert result["id"] == "28457315" + assert result["symbol"] == "ETH/USDT" + assert result["side"] == "buy" + assert result["type"] == "limit" + assert result["status"] == "open" + + +# ── Real ccxt exchange integration ────────────────────────────────────────── + +class TestCcxtExchangePatching: + """Test patching on real ccxt exchange instances (all major exchanges). + + Note: ccxt's native hmac() expects (request: bytes, secret: bytes, algorithm=hashlib.sha256). + Our _patch_signing wraps it to also accept (request: str, secret: str, algorithm: str|None). + Tests here use the patched interface. + """ + + EXCHANGES = ["binance", "bybit", "okx", "kucoin", "bitget", "kraken"] + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_creates_and_patches_exchange(self, exchange_id): + """Each exchange should be created with patched HMAC.""" + exchange = create_exchange(exchange_id, {"apiKey": "test", "secret": "test"}) + cls = getattr(ccxt, exchange_id) + assert isinstance(exchange, cls) + # hmac should be our patched version + assert exchange.hmac.__name__ == "fast_hmac" + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_patched_hmac_sha256_produces_correct_result(self, exchange_id): + """Patched exchange.hmac should produce correct SHA256 for known inputs.""" + patched = create_exchange(exchange_id, {"apiKey": "test", "secret": "test"}) + + request = b"timestamp=1700000000000&symbol=BTCUSDT" + secret = b"my_secret_key_for_testing" + + patched_result = patched.hmac(request, secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, request, hashlib.sha256).hexdigest() + + assert patched_result == expected, ( + f"{exchange_id}: patched={patched_result} != expected={expected}" + ) + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_patched_hmac_sha512_matches_stdlib(self, exchange_id): + """SHA512 patched hmac should match stdlib output.""" + patched = create_exchange(exchange_id, {"apiKey": "test", "secret": "test"}) + + request = b"test_data" + secret = b"test_secret" + + patched_result = patched.hmac(request, secret, "sha512", "hex") + expected = stdlib_hmac.new(secret, request, hashlib.sha512).hexdigest() + + assert patched_result == expected + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_patched_hmac_non_hex_falls_back(self, exchange_id): + """Non-hex digest should fall back to original ccxt implementation.""" + patched = create_exchange(exchange_id, {"apiKey": "test", "secret": "test"}) + + request = b"test_data" + secret = b"test_secret" + + # binary digest + patched_result = patched.hmac(request, secret, "sha256") + expected = stdlib_hmac.new(secret, request, hashlib.sha256).digest() + # fast_hmac with algorithm="sha256" returns hex by default + # but the fallback returns binary with no digest arg + assert isinstance(patched_result, (str, bytes)) + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_exchange_has_expected_methods(self, exchange_id): + """Created exchanges should expose standard ccxt methods.""" + exchange = create_exchange(exchange_id, {}) + assert hasattr(exchange, "fetch_ticker") + assert hasattr(exchange, "fetch_order_book") + assert hasattr(exchange, "fetch_ohlcv") + assert hasattr(exchange, "create_order") + assert hasattr(exchange, "cancel_order") + assert hasattr(exchange, "fetch_balance") + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_exchange_config_forwarded(self, exchange_id): + """Config dict should be applied to the exchange instance.""" + exchange = create_exchange(exchange_id, { + "timeout": 7777, + "rateLimit": 500, + "enableRateLimit": True, + }) + assert exchange.timeout == 7777 + assert exchange.rateLimit == 500 + assert exchange.enableRateLimit is True + + +class TestCcxtAsyncExchangePatching: + """Test patching on async ccxt exchange instances.""" + + EXCHANGES = ["binance", "bybit", "okx"] + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_creates_async_exchange(self, exchange_id): + """async_mode=True should return ccxt.async_support exchange.""" + exchange = create_exchange(exchange_id, {}, async_mode=True) + cls = getattr(ccxt_async, exchange_id) + assert isinstance(exchange, cls) + + @pytest.mark.parametrize("exchange_id", EXCHANGES) + def test_async_patched_hmac_matches_sync(self, exchange_id): + """Async and sync exchanges should produce identical HMAC results.""" + sync_ex = create_exchange(exchange_id, {"apiKey": "k", "secret": "s"}) + async_ex = create_exchange(exchange_id, {"apiKey": "k", "secret": "s"}, async_mode=True) + + request = b"param1=val1¶m2=val2" + secret = b"the_secret" + + sync_result = sync_ex.hmac(request, secret, "sha256", "hex") + async_result = async_ex.hmac(request, secret, "sha256", "hex") + + assert sync_result == async_result + + +# ── Signing consistency across multiple calls ─────────────────────────────── + +class TestSigningConsistency: + """Verify that repeated signing produces identical results (no state leaks).""" + + def test_repeated_hmac_is_deterministic(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + request = b"symbol=BTCUSDT×tamp=1700000000000" + secret = b"my_api_secret" + + results = [exchange.hmac(request, secret, "sha256", "hex") for _ in range(100)] + assert len(set(results)) == 1, "HMAC produced different results across calls" + + def test_different_inputs_produce_different_outputs(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"same_secret" + + results = set() + for i in range(50): + result = exchange.hmac(f"timestamp={i}".encode(), secret, "sha256", "hex") + results.add(result) + + assert len(results) == 50, "Different inputs should produce unique HMACs" + + def test_hmac_output_is_valid_hex(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac(b"data", b"secret", "sha256", "hex") + # SHA256 hex = 64 chars + assert len(result) == 64 + assert all(c in "0123456789abcdef" for c in result) + + +# ── Edge cases ────────────────────────────────────────────────────────────── + +class TestEdgeCases: + """Test unusual inputs and boundary conditions.""" + + def test_unicode_in_request(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac("price=1000¬e=日本語テスト".encode(), b"secret", "sha256", "hex") + assert len(result) == 64 + + def test_very_long_request(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + long_request = b"x" * 100_000 + result = exchange.hmac(long_request, b"secret", "sha256", "hex") + assert len(result) == 64 + + def test_empty_request_and_secret(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + result = exchange.hmac(b"", b"", "sha256", "hex") + expected = stdlib_hmac.new(b"", b"", hashlib.sha256).hexdigest() + assert result == expected + + def test_special_chars_in_secret(self): + exchange = create_exchange("binance", {"apiKey": "k", "secret": "s"}) + secret = b"key+with/special=chars&and%20encoding" + result = exchange.hmac(b"data", secret, "sha256", "hex") + expected = stdlib_hmac.new(secret, b"data", hashlib.sha256).hexdigest() + assert result == expected + + def test_normalize_ticker_with_none_values(self): + raw = {"symbol": "BTC/USDT", "last": None, "bid": None, "ask": None, + "high": None, "low": None, "baseVolume": None, "timestamp": None} + result = normalize_ticker(raw) + assert result["symbol"] == "BTC/USDT" + assert result["last"] is None + assert result["volume"] is None + + def test_normalize_ticker_with_numeric_strings(self): + raw = {"last": "0.00000001", "bid": "0", "ask": "999999999.99"} + result = normalize_ticker(raw) + assert result["last"] == 1e-8 + assert result["bid"] == 0.0 + assert result["ask"] == 999999999.99 + + def test_normalize_order_with_mixed_types(self): + raw = {"price": 2050, "amount": "1.5", "filled": 0.5, "remaining": None} + result = normalize_order(raw) + assert result["price"] == 2050.0 + assert result["amount"] == 1.5 + assert result["filled"] == 0.5 + assert result["remaining"] is None + + +# ── Binance-specific signature tests ──────────────────────────────────────── + +class TestBinanceSignature: + """Test Binance API signature generation end-to-end.""" + + BINANCE_SECRET = b"NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j" + BINANCE_QUERY = b"symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" + BINANCE_EXPECTED = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" + + def test_binance_official_vector_via_exchange(self): + """Verify patched exchange produces correct Binance API signature.""" + exchange = create_exchange("binance", {"apiKey": "test", "secret": "test"}) + result = exchange.hmac(self.BINANCE_QUERY, self.BINANCE_SECRET, "sha256", "hex") + assert result == self.BINANCE_EXPECTED + + def test_binance_signature_via_direct_hmac(self): + """Verify direct hmac_sha256 matches Binance vector.""" + result = hmac_sha256(self.BINANCE_SECRET, self.BINANCE_QUERY) + assert result.hex() == self.BINANCE_EXPECTED + + def test_consistency_between_direct_and_exchange(self): + """Direct hmac and exchange.hmac should produce the same result.""" + exchange = create_exchange("binance", {"apiKey": "test", "secret": "test"}) + + direct = hmac_sha256(self.BINANCE_SECRET, self.BINANCE_QUERY).hex() + via_exchange = exchange.hmac(self.BINANCE_QUERY, self.BINANCE_SECRET, "sha256", "hex") + + assert direct == via_exchange diff --git a/pants.toml b/pants.toml index 5507630dd0..c633cbfb0e 100644 --- a/pants.toml +++ b/pants.toml @@ -22,6 +22,7 @@ root_patterns = [ "/packages/backtesting", "/packages/binary", "/packages/commons", + "/packages/edge", "/packages/evaluators", "/packages/node", "/packages/flow",