diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..fce1c26 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..274d66a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..023adfc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Default owner for everything +* @ignromanov + +# Phase 3+: add @void-layer/maintainers when team formed diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8e6ed4c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + changesets: + patterns: ["@changesets/*"] + typescript: + patterns: ["typescript", "@types/*"] + - package-ecosystem: "cargo" + directory: "/packages/codec" + schedule: + interval: "weekly" + open-pull-requests-limit: 3 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..449e902 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + lint-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: { version: 10.24.0 } + - uses: actions/setup-node@v4 + with: { node-version: 24, cache: pnpm } + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: { rustflags: "" } + - uses: Swatinem/rust-cache@v2 + - run: rustup target add wasm32-unknown-unknown + - name: Install wasm-pack 0.14.1 + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | WASM_PACK_VERSION=v0.14.1 sh + - run: pnpm install --frozen-lockfile + - run: pnpm -r build + - run: cargo build --manifest-path packages/codec/Cargo.toml --release + - run: cargo test --manifest-path packages/codec/Cargo.toml + - name: Assert size budgets + run: bash scripts/assert-size.sh + working-directory: packages/codec + - run: | + for dir in packages/codec packages/types packages/networks; do + (cd "$dir" && npm pack --dry-run) + done + vector-parity: + needs: [lint-and-build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: { version: 10.24.0 } + - uses: actions/setup-node@v4 + with: { node-version: 24, cache: pnpm } + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: { rustflags: "" } + - uses: Swatinem/rust-cache@v2 + - run: rustup target add wasm32-unknown-unknown + - name: Install wasm-pack 0.14.1 + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | WASM_PACK_VERSION=v0.14.1 sh + - run: pnpm install --frozen-lockfile + - run: pnpm -C packages/codec build + - name: TS/JS parity (vitest) + run: pnpm -C packages/codec exec vitest run tests/parity.test.ts + - name: Rust parity (cargo) + run: cargo test --manifest-path packages/codec/Cargo.toml --test parity + macos-sanity: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: Swatinem/rust-cache@v2 + - run: cargo test --manifest-path packages/codec/Cargo.toml + test-wasm-node: + needs: [lint-and-build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: { rustflags: "" } + - uses: Swatinem/rust-cache@v2 + - run: rustup target add wasm32-unknown-unknown + - name: Install wasm-pack 0.14.1 + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | WASM_PACK_VERSION=v0.14.1 sh + - name: Run wasm-pack test --node (AC-9 boundary tests) + run: wasm-pack test --node packages/codec diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..256e4a6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Release +on: + push: + branches: [main] +permissions: + id-token: write + contents: write + pull-requests: write +jobs: + validate-oidc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 24 } + - run: | + echo "Phase 1: release.yml plumbing reserved. Publish job lands Phase 3." + echo "OIDC token presence: $([ -n "$ACTIONS_ID_TOKEN_REQUEST_URL" ] && echo yes || echo no)" diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..145d3fa --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +save-exact=true +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..66d7910 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +pkg/ +target/ +.changeset/ +pnpm-lock.yaml +Cargo.lock +*.md diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4d2523a --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8cafa2d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# Code of Conduct + +This project adopts the [Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) as its Code of Conduct. + +Please read the full text at the link above before contributing. + +## Reporting + +Report unacceptable behavior to **ign.romanov@gmail.com** with subject prefix `[code-of-conduct][@void-layer/codec]`. + +Reports are reviewed within 72 hours. Confidentiality is maintained for reporters. + +## Scope + +This Code of Conduct applies within all project spaces — GitHub issues, pull requests, discussions, and any official communication channels — and when an individual is officially representing the project. + +## Enforcement + +Maintainers may take any action deemed appropriate, including warning, temporary ban, or permanent ban from the project. + +## Attribution + +This document is a pointer to the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/), which is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3fb1b1a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to @void-layer/codec + +## Dev Setup + +```bash +# Requires Node >=24 and pnpm >=10 +pnpm install +``` + +## Making Changes + +Build all packages: +```bash +pnpm build +``` + +Run tests: +```bash +pnpm test +``` + +Run lint: +```bash +pnpm lint +``` + +## Releasing + +This repo uses [Changesets](https://github.com/changesets/changesets) for versioning. + +Add a changeset for any user-facing change: +```bash +pnpm changeset +``` + +Then commit the generated `.changeset/*.md` file with your PR. + +Maintainers run `pnpm version` to bump versions and `pnpm release` to publish. + +## Design Rationale + +See [spec 056](https://github.com/ignromanov/voidpay-ai/blob/master/ops/specs/056-void-layer-codec-extraction/spec.md). + +The decision to rewrite the codec from TypeScript to Rust+WASM is documented in: +`voidpay-ai/agent-memory/advisors/decisions/2026-05-09-kai-cto-codec-rust-supersedes-ts-first.md` + +## Schema + +v1 schema is LOCKED. Old invoice URLs must decode forever. Never break existing field assignments. diff --git a/README.md b/README.md index aa097f9..d8264ea 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,56 @@ # @void-layer/codec -Canonical Invoice codec — TLV + Brotli wire format. v1 schema LOCKED. +Canonical Invoice codec — TLV + Brotli wire format. v1 schema LOCKED forever. -[![npm version](https://img.shields.io/npm/v/@void-layer/codec)](https://www.npmjs.com/package/@void-layer/codec) -[![CI](https://github.com/void-layer/codec/actions/workflows/ci.yml/badge.svg)](https://github.com/void-layer/codec/actions/workflows/ci.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![npm](https://img.shields.io/npm/v/@void-layer/codec.svg)](https://npmjs.com/package/@void-layer/codec) [![CI](https://github.com/void-layer/codec/actions/workflows/ci.yml/badge.svg)](https://github.com/void-layer/codec/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ## Status -Phase 1 scaffolding — Rust impl coming Phase 2. - -See [spec 056](https://github.com/ignromanov/voidpay-ai/blob/master/ops/specs/056-void-layer-codec-extraction/spec.md) for full design rationale. +🚧 Phase 1 scaffolding (May 2026) — Rust impl lands Phase 2 ## Packages -| Package | Description | Status | -|---------|-------------|--------| -| `@void-layer/codec` | Rust+WASM TLV encoder/decoder | Phase 1 init | -| `@void-layer/types` | Shared TypeScript types | Phase 1 init | -| `@void-layer/networks` | Chain configs + token list | Phase 1 init | +| Package | Status | Description | +|---------|--------|-------------| +| `@void-layer/codec` | Phase 1 | Rust + WASM canonical TLV codec | +| `@void-layer/types` | Phase 1 | Manual TypeScript types (zero runtime deps) | +| `@void-layer/networks` | Phase 1 | EVM chain configs + token list (no RPC keys) | + +## Quick Install + +```bash +pnpm add @void-layer/codec +``` + +> Not yet published — Phase 3 + +## Why + +- Third-party developers building on top of VoidPay need a stable, versioned codec they can depend on +- MCP servers, Farcaster Frames, and AI agents all depend on a common wire format — language-agnostic TLV is the right primitive +- Version-controlled schema means consumers can pin to v1 and get backward-compat guarantees forever +- Language-agnostic TLV encoding allows Rust, Go, Python, and JS implementations to interoperate on the same wire format + +## Constitution IV — Perpetual + +> Schema v1 LOCKED. Old URLs decode forever. + +## Development + +See [CONTRIBUTING.md](CONTRIBUTING.md) + +## Security + +See [SECURITY.md](SECURITY.md) + +## Architecture + +See [docs/architecture-overview.md](docs/architecture-overview.md) + +## Spec + +Full design: [spec 056 in voidpay-ai](https://github.com/ignromanov/voidpay-ai/blob/master/ops/specs/056-void-layer-codec-extraction/spec.md) (private — internal reference) -## Contributing +--- -See [CONTRIBUTING.md](CONTRIBUTING.md). +Built by [Ignat Romanov](https://github.com/ignromanov) · MIT License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6a15be6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ +# Security Policy + +## Supported Versions + +Latest published version on npm only (Phase 3+). + +| Version | Supported | +|---------|-----------| +| latest | ✅ | +| < latest | ❌ | + +## Reporting a Vulnerability + +**Preferred**: open a private advisory at https://github.com/void-layer/codec/security/advisories/new + +**Email fallback**: `ign.romanov@gmail.com` with subject prefix `[security][@void-layer/codec]` + +**Response SLA**: 72 hours initial acknowledgment. + +## Scope + +### In scope + +- Codec encoding/decoding correctness +- Schema v1 backward-compatibility violations +- BigInt boundary issues (precision loss, silent truncation) +- WASM initialization security (race conditions, init bypass) +- Wire format determinism (canonical hash drift) + +### Out of scope + +- VoidPay product application — see [voidpay/SECURITY.md](https://github.com/ignromanov/voidpay/blob/master/SECURITY.md) +- RPC provider issues — those are external infrastructure + +## Constitution VI + +RPC keys are server-side only. `@void-layer/*` packages NEVER contain RPC keys or PII. + +## Provenance + +All releases from Phase 3+ ship with npm Provenance attestations via Trusted Publishing. diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md new file mode 100644 index 0000000..ed91c04 --- /dev/null +++ b/docs/architecture-overview.md @@ -0,0 +1,64 @@ +# @void-layer Architecture Overview + +## Monorepo Structure + +``` +packages/ +├─ codec/ # @void-layer/codec — Rust + WASM canonical TLV codec +├─ types/ # @void-layer/types — manual TS types (zero runtime deps) +└─ networks/ # @void-layer/networks — chain configs + token list (no RPC keys) +``` + +## Dependency Rules (Immutable) + +- `@void-layer/codec` depends on: **nothing** (pure Rust + auto-gen TS bindings) +- `@void-layer/types` depends on: **nothing** (pure TS, no runtime deps) +- `@void-layer/networks` depends on: `@void-layer/types` only +- Downstream packages (agent, merchant, frame) depend on codec + types + networks +- Auto-generated types from `wasm-bindgen` + `tsify` live in `@void-layer/codec/types` subpath export — NOT in `@void-layer/types` + +## Build Pipeline (Phase 2+) + +``` +src/*.rs → cargo + wasm-pack → pkg/ + ├─ codec.js (ESM) + ├─ codec.d.ts (auto-gen TS bindings via tsify) + └─ codec_bg.wasm + +CJS wrapper hand-authored: cjs/index.js (await init() guard) +``` + +## Schema Versioning + +- **v1 LOCKED** (Constitution IV). Old URLs decode forever. +- **v2 additive** via TLV odd/even rule + `extensions` map (BOLT12 import). +- **Receipt-hash**: `keccak256(canonical_binary_PRE_compression)` (algo-agnostic). + +## Compression + +- **Wire format v1**: Brotli q11 whole-payload, signaled by `VERSION & 0x80` (LOCKED). +- **v2 runtime branch** (B-iv per spec §3.16): + 1. `'brotli' in CompressionStream.supportedFormats` → native (zero bundle cost) + 2. Else → `brotli-wasm` peerDep fallback (current shipping pattern) + +## Encoding + +- URL hash fragment: `base64url` (LOCKED v1; default v2) +- QR alphanumeric: `Crockford32` (v1.3+, gated on >15% QR share analytics) +- EVM calldata: `hex` +- Solana account data: `base58` + +## Hard Limits + +- WASM blob: <80 KB +- npm package total: <200 KB +- URL max: 2000 bytes compressed +- Notes max: 280 chars + +## References + +- Full spec: `voidpay-ai/ops/specs/056-void-layer-codec-extraction/spec.md` +- ADR-supersession: `voidpay-ai/agent-memory/advisors/decisions/2026-05-09-kai-cto-codec-rust-supersedes-ts-first.md` +- Constitution: VoidPay Principle IV (Perpetual + Schema versioning) +- TLV Registry: [`packages/codec/REGISTRY.md`](../packages/codec/REGISTRY.md) +- TLV contribution guide: [`contributing-tlv-registry.md`](./contributing-tlv-registry.md) diff --git a/docs/contributing-tlv-registry.md b/docs/contributing-tlv-registry.md new file mode 100644 index 0000000..adb182e --- /dev/null +++ b/docs/contributing-tlv-registry.md @@ -0,0 +1,61 @@ +# Contributing to the TLV Registry + +## Overview + +TLV Type IDs are **append-only forever**. Once allocated, never reused or reordered (Constitution IV — "Old URLs decode forever"). + +## TLV Type Ranges + +The canonical source-of-truth lives in [`packages/codec/REGISTRY.md`](../packages/codec/REGISTRY.md). Summary: + +| Range | Purpose | Status | +|-------|---------|--------| +| 1–13 | v1 core fields | LOCKED (Constitution IV) | +| 14 | ITEMS | LOCKED | +| 15–99 | VoidPay canonical core | v2 extensions (mandatory=even, optional=odd) | +| 100–199 | Agent-economy extensions | parentHash, budgetCap, delegationScope, split | +| 200–999 | Reserved canonical | future on-chain anchors, lifecycle, privacy | +| 1000–9999 | Vendor namespace | PR-merged FCFS | +| 10000+ | Experimental / reclaimable | 12-month inactivity policy | + +## BOLT12 odd/even rule + +- **Even** TLV types are mandatory — unknown even type → decode error +- **Odd** TLV types are optional — unknown odd type → ignore and pass through + +This enables forward compatibility: future codecs add odd TLV types that older decoders skip cleanly. + +## How to Allocate + +1. **Pick a range** matching your use case (see table above) +2. **Open a PR** titled `[TLV] allocate : for ` +3. **PR body MUST include**: + - Motivation (why this allocation, who uses it) + - Encoding spec (byte-level layout: type → length → value) + - Backward-compatibility statement (does it break v1 decoders?) + - Test vector (encoded hex + decoded JSON example) +4. **Vendor namespace allocations** MUST use `vendor..` sub-key convention to prevent collisions +5. **Editor reviews PR and merges** when well-formed + +## Vendor Squatting Policy + +Per spec §4.4: **12-month inactivity reclaim**. Unused vendor allocations may be reclaimed by editors after 12 months of no on-chain or on-protocol activity, with 30-day PR notice on the registry. + +## Editor Role + +Per EIP-1 verbatim: **administrative only**. Editors merge well-formed PRs that respect ranges and naming. They do NOT pass judgment on feature value. + +Magicians-style governance forum is deferred until ≥3 external implementations exist (premature governance theatre at our scale). + +## Phase 1 Editor + +- @ignromanov + +## Future (Phase 3+) + +When the codec ecosystem matures, editor responsibilities migrate to a `@void-layer/maintainers` team. + +## References + +- Spec 056 §4.4 (TLV Registry — BOLT-Style Federated) +- [`packages/codec/REGISTRY.md`](../packages/codec/REGISTRY.md) — canonical type-ID source-of-truth diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..467bfa4 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,36 @@ +// @ts-check +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/pkg/**', + '**/target/**', + '.changeset/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['packages/*/src/**/*.ts'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + // Constitution VI — no RPC keys in source + 'no-restricted-syntax': [ + 'error', + { + selector: "Literal[value=/alch_|alchemyapi\\.io\\/v2\\/|infura\\.io\\/v3\\//]", + message: 'RPC keys must never appear in @void-layer source (Constitution VI). Server-side only in voidpay.xyz.', + }, + ], + }, + }, +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9a999a --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "@void-layer/monorepo", + "private": true, + "engines": { + "node": ">=24", + "pnpm": ">=10" + }, + "scripts": { + "build": "pnpm -r --filter './packages/*' build", + "test": "pnpm -r --filter './packages/*' test", + "lint": "pnpm -r --filter './packages/*' lint", + "changeset": "changeset", + "version": "changeset version", + "release": "pnpm build && changeset publish" + }, + "devDependencies": { + "@changesets/cli": "^2.27.0", + "@eslint/js": "^9.39.4", + "eslint": "^9.39.4", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4" + }, + "packageManager": "pnpm@10.24.0" +} diff --git a/packages/codec/.clippy.toml b/packages/codec/.clippy.toml new file mode 100644 index 0000000..5f7f861 --- /dev/null +++ b/packages/codec/.clippy.toml @@ -0,0 +1,2 @@ +msrv = "1.85.0" +# Phase 2 tightens lint level (deny pedantic warnings) diff --git a/packages/codec/.npmignore b/packages/codec/.npmignore new file mode 100644 index 0000000..1be1a11 --- /dev/null +++ b/packages/codec/.npmignore @@ -0,0 +1,10 @@ +# Source and build artifacts — excluded from npm package +src/ +Cargo.toml +Cargo.lock +target/ +vectors/ +docs/ +.cargo/ + +# Included in published package: pkg/, cjs/, README.md, LICENSE, REGISTRY.md diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock new file mode 100644 index 0000000..8dc83ba --- /dev/null +++ b/packages/codec/Cargo.lock @@ -0,0 +1,1027 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +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 = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[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 = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[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-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.6", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[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 = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ruint" +version = "1.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +dependencies = [ + "ruint-macro", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[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 = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "void-layer-codec" +version = "0.1.0" +dependencies = [ + "dlmalloc", + "js-sys", + "phf", + "proptest", + "ruint", + "serde", + "serde-wasm-bindgen", + "serde_json", + "thiserror", + "tiny-keccak", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +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/codec/Cargo.toml b/packages/codec/Cargo.toml new file mode 100644 index 0000000..fd00ecf --- /dev/null +++ b/packages/codec/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "void-layer-codec" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "Canonical Invoice codec — TLV wire format, Brotli via JS shim" +repository = "https://github.com/void-layer/codec" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +thiserror = "2" +phf = { version = "0.11", features = ["macros"] } +ruint = { version = "1", default-features = false } +tiny-keccak = { version = "2", features = ["keccak"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +dlmalloc = { version = "0.2", features = ["global"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" +js-sys = "0.3" +serde_json = "1" + +# proptest pulls getrandom 0.3 / wait-timeout which don't build for wasm32 — see spec 056 plan-2c C8. +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +proptest = "1" + +[profile.release] +opt-level = "z" +lto = "fat" +codegen-units = 1 +strip = "symbols" +panic = "abort" +incremental = false diff --git a/packages/codec/README.md b/packages/codec/README.md new file mode 100644 index 0000000..45cce3f --- /dev/null +++ b/packages/codec/README.md @@ -0,0 +1,52 @@ +# @void-layer/codec + +> **Status**: Phase 1 scaffolding. Rust + WASM implementation lands Phase 2. + +Canonical Invoice codec — TLV + Brotli wire format. v1 schema LOCKED (old URLs decode forever). + +## Install + +```bash +npm install @void-layer/codec brotli-wasm +``` + +`brotli-wasm` is a required peer dependency. + +## API (Phase 2 placeholder) + +```ts +import { encode, decode } from '@void-layer/codec'; + +// encode: Invoice -> Uint8Array (TLV + Brotli compressed) +const bytes = encode(invoice); + +// decode: Uint8Array -> Invoice (version-aware, v1 LOCKED) +const invoice = decode(bytes); +``` + +Full API defined in spec 056 §3.6. TypeScript bindings auto-generated from Rust via `wasm-bindgen` + `tsify`. + +## Packages + +| Package | Description | +|---------|-------------| +| `@void-layer/codec` | This package — Rust/WASM codec | +| `@void-layer/types` | Manual TypeScript types | +| `@void-layer/networks` | Chain configs (5 EVM chains) | + +## Design + +- Wire format: TLV (BOLT12-style) + Brotli compression +- Output: `<2B magic> <1B kind> ` +- v1 schema: LOCKED. Old invoice URLs decode forever. +- peerDep strategy: brotli-wasm (runtime branch, see spec §3.16) + +## Links + +- [Spec 056](https://github.com/ignromanov/voidpay-ai/blob/main/ops/specs/056-void-layer-codec-extraction/spec.md) +- [TLV Registry](./REGISTRY.md) +- [Bundle Budget](./docs/bundle-budget.md) + +## License + +MIT diff --git a/packages/codec/REGISTRY.md b/packages/codec/REGISTRY.md new file mode 100644 index 0000000..4fc84e2 --- /dev/null +++ b/packages/codec/REGISTRY.md @@ -0,0 +1,44 @@ +# TLV Registry — @void-layer/codec + +> Canonical source-of-truth for TLV type range allocations. +> Governance model: BOLT-style federated (GitHub PR-driven, FCFS for vendor namespace). +> Per spec 056 §4.4. + +## TLV Type Ranges — Invoice Message Kind (0x01) + +``` +1–13 v1 core fields [LOCKED — Constitution IV] +14 ITEMS [LOCKED] +15–99 VoidPay canonical core [v2 core extensions; mandatory=even, optional=odd] +100–199 Agent-economy extensions [parentHash, budgetCap, delegationScope, split, ...] +200–999 Reserved canonical [future on-chain anchors, lifecycle, privacy disclosure] +1000–9999 Vendor namespace [vendor..* — PR-merged FCFS] +10000+ Experimental / reclaimable [12-month inactivity → reclaim policy] +``` + +## Vendor Namespace Governance + +- Vendor entries follow the naming convention: `vendor..` +- Allocation is first-come-first-served via GitHub PR +- Editor role is administrative only (per EIP-1 verbatim — editors don't pass judgment) +- Magicians-style forum deferred until ≥3 external implementations exist + +## Vendor Squatting Reclaim Policy + +Any vendor namespace entry (type range 1000–9999) that has had **no activity** (no merged PR, no published release referencing the type, no tracked usage) for **12 consecutive months** is eligible for reclamation. Reclaim process: + +1. Maintainer opens a GitHub issue tagging the original allocatee +2. 30-day comment period +3. If no response: PR removes the vendor entry; type ID becomes available for re-allocation +4. Original allocatee may re-request the same ID within 90 days of reclaim if they demonstrate active use + +## Per-record Allocation + +Each spec's PR proposes specific Type IDs in the appropriate range: +- Spec 057 (lateFee) PR → proposes Type in 100–199 range +- Spec 060 (agent) PR → proposes parentHash, budgetCap etc. in 100–199 +- Spec 064 (contract) PR → proposes contractAddress, proofIndex in 200–999 + +## Allocated Entries + +_No entries yet. Phase 1 scaffolding._ diff --git a/packages/codec/docs/bundle-budget.md b/packages/codec/docs/bundle-budget.md new file mode 100644 index 0000000..a8a92ae --- /dev/null +++ b/packages/codec/docs/bundle-budget.md @@ -0,0 +1,29 @@ +# Bundle Budget — @void-layer/codec v0.1.0 (Phase 2) + +> Architecture: B-v — Brotli lives in the JS shim (`dist/index.js`), NOT in the WASM. +> The WASM exposes only canonical encode/decode + receiptHash. +> Gzip is the gated metric (spec §3). + +| Component | Bytes | Cap | Margin | +|-----------|-------|-----|--------| +| `void_layer_codec_bg.wasm` raw | 181,457 | — | — | +| `void_layer_codec_bg.wasm` gzip | 79,486 | 81,920 (80 KB) | ~3% | +| Package tarball (`pkg/` + `dist/`) | 92,160 | 204,800 (200 KB) | ~55% | + +## Notes + +- **gzip figure vs earlier ~73 KB**: the increase is due to the U256/ruint widening + added for full BigInt support (spec §D-B8). ruint brings additional lookup tables + and arithmetic paths that add ~6 KB gzip. +- **No brotli-decompressor row**: Brotli decompression is NOT in the WASM (B-v + decision, 2026-05-20). The JS shim (`dist/index.js`) imports `brotli-wasm` as a + peer dependency and handles compression/decompression outside the WASM boundary. +- **Anti-stop guard**: if a future change pushes gzip over 81,920 bytes, halt and + report to Kai. Do NOT raise the cap unilaterally. + +## Caps (spec §3) + +| Gate | Cap | +|------|-----| +| WASM gzip | 81,920 bytes (80 KB) | +| Package tarball | 204,800 bytes (200 KB) | diff --git a/packages/codec/docs/golden-vectors.md b/packages/codec/docs/golden-vectors.md new file mode 100644 index 0000000..6595798 --- /dev/null +++ b/packages/codec/docs/golden-vectors.md @@ -0,0 +1,174 @@ +# Golden Vectors — `vectors/v4-codec.json` + +> **Append-only forever.** Once a vector is committed, its `name`, `canonical_hex`, +> and `wire_hex` are immutable. The only permitted change is adding new vectors at +> the end of the array. Amending an existing vector is a Constitution IV violation. + +--- + +## Purpose + +Golden vectors are the wire-format regression suite for `@void-layer/codec`. They +serve three functions: + +1. **Byte-stable reference** — any future codec implementation (Rust, TS, Python, + Go) must produce identical `canonical_hex` bytes for the same `decoded` input. +2. **Parity gate** — the `vector-parity` CI job (T-P2-13) loads `v4-codec.json` + and asserts both directions × both forms (canonical + wire) in Rust and TS. +3. **Perpetuity proof** — URLs generated today must decode correctly in any future + version. The vectors are the machine-readable proof of that contract. + +--- + +## Schema (`schema_version: 1`) + +### Non-malformed vector + +```jsonc +{ + "name": "minimal-single-tlv", // stable identifier, kebab-case + "canonical_hex": "5601...", // hex of encodeInvoiceCanonical output + "wire_hex": "5601...", // hex of encodeInvoiceWire output + "decoded": { ... }, // the Invoice object (source of truth) + "roundtrip": true, // decode(encode(decoded)) === decoded + "diagnostic": "..." // human-readable note +} +``` + +`wire_hex` is Brotli-compressed (VERSION byte has `0x80` set) when Brotli reduces +the payload size. For small invoices Brotli expands, so `wire_hex === canonical_hex` +and the `COMPRESSED_FLAG` is NOT set — per C4 amendment (2026-05-20). Both fields +are always present regardless. + +### Malformed vector — decode-input subtype + +```jsonc +{ + "name": "malformed-bad-magic", + "canonical_hex": "ff01...", // OR wire_hex — whichever layer the error targets + "diagnostic": "malformed:canonical", // or "malformed:wire" + "expected_error": "BadMagic" +} +``` + +Decode-input malformed vectors carry one hex field (`canonical_hex` or `wire_hex`) +and no `decoded` field. Feed the bytes to the decoder; assert the named error variant. + +### Malformed vector — encode-input subtype + +```jsonc +{ + "name": "bigint-amount-over-u256", + "decoded": { "total": "115792...", ... }, // full Invoice + "diagnostic": "malformed:encode-input", + "expected_error": "InvalidAmount" +} +``` + +Encode-input malformed vectors carry a `decoded` Invoice and no hex fields. The error +fires at encode time — no bytes are produced. Construct the Invoice from `decoded` and +assert that `encodeInvoiceCanonical` throws the named error variant. + +`diagnostic` prefix summary: +- `malformed:canonical` — decode `canonical_hex` → expect error +- `malformed:wire` — decode `wire_hex` → expect error +- `malformed:encode-input` — encode `decoded` Invoice → expect error + +--- + +## Starter Set (v4-codec.json, schema_version=1) + +18 vectors, regenerated 2026-05-20 for U256 widening (T-P2-12a / C9 amendment) and +corrected malformed vector set (T-P2-12 follow-up, Kai decision 2026-05-20). + +| # | Name | Category | wire compressed | +|---|------|----------|----------------| +| 1 | `minimal-single-tlv` | Minimal | false | +| 2 | `chain-ethereum` | Chain selector | false | +| 3 | `chain-base` | Chain selector | false | +| 4 | `chain-arbitrum` | Chain selector | false | +| 5 | `chain-optimism` | Chain selector | false | +| 6 | `chain-polygon` | Chain selector | false | +| 7 | `bigint-amount-zero` | BigInt edge | false | +| 8 | `bigint-amount-one` | BigInt edge | false | +| 9 | `bigint-amount-uint256-max` | BigInt edge | **true** | +| 10 | `bigint-amount-over-u256` | BigInt edge (malformed — InvalidAmount) | — | +| 11 | `malformed-checksum-mismatch` | Malformed | — | +| 12 | `malformed-varint-overflow` | Malformed | — | +| 13 | `extension-magic-dust` | Extension | **true** | +| 14 | `extension-og-param` | Extension | **true** | +| 15 | `extension-sub-invoice-chain` | Extension | false | +| 16 | `malformed-corrupted-brotli` | Malformed | — | +| 17 | `malformed-oversize` | Malformed | — | +| 18 | `malformed-bad-magic` | Malformed | — | + +**Changes from initial 16-vector set (C9 amendment, 2026-05-20)**: +- `bigint-amount-u128-max` replaced by `bigint-amount-uint256-max` (U256::MAX = + `115792089237316195423570985008687907853269984665640564039457584007913129639935`). + After U256 widening this encodes successfully (roundtrip=true, wire compressed). +- `bigint-amount-over-u256` added: amount = 2^256, encode rejects with `InvalidAmount`. + No canonical_hex field — error fires at encode time, no bytes produced. + +**Changes from 17-vector set (T-P2-12 follow-up, Kai decision 2026-05-20)**: +- `malformed-varint-overflow` corrected: the previous hex (`56 01 01 18 0x26 38×0x80`) + was misidentified — the codec hits `ChecksumMismatch` before any varint overflow path. + The old hex is preserved as `malformed-checksum-mismatch` (new name, same bytes). +- New `malformed-varint-overflow` added: hex = `56 01 01 18` + 37×`0x80`. The LENGTH + field of the first TLV record is 37 continuation bytes with no terminal byte. The + varint decoder fires `VarintOverflow` at `bytes_read == MAX_BYTES (37)` before the + checksum stage. Empirically confirmed on both WASM and Rust surfaces. + +**Why some vectors are uncompressed**: the T-P2-0a Brotli spike measured that +payloads under ~180 bytes expand under Brotli q11. All single-item minimal invoices +fall below this threshold. The `bigint-amount-uint256-max`, `extension-magic-dust`, +and `extension-og-param` vectors are compressed due to larger payloads. + +--- + +## Append-Only Rule + +Adding new vectors (at the end of the array) is always safe. + +The following operations are FORBIDDEN: +- Changing `name`, `canonical_hex`, or `wire_hex` of any existing vector +- Reordering vectors +- Removing vectors +- Changing `schema_version` (a new schema gets a new file, e.g. `v4-codec-v2.json`) + +If you need to correct a vector that has never been published in an npm release, +open a PR, reference the Kai decision that approves the correction, and include a +`BREAKING` note in the changeset. + +--- + +## Regenerating + +The generator is `scripts/generate-vectors.ts`. It imports from `pkg-node/` +(nodejs-target WASM build) and mirrors the `src/index.ts` shim wire logic. + +```bash +# From packages/codec root: +pnpm build:nodejs # rebuild pkg-node/ if Rust changed +pnpm generate-vectors # runs scripts/run-generate-vectors.test.ts via dedicated config +``` + +`pnpm test` intentionally excludes `scripts/**` — regeneration is always explicit. + +Regeneration replaces the file. Diff the output carefully before committing — +any change to an existing vector's hex fields is a perpetuity violation. + +--- + +## CodecError variants (expected_error values) + +| Variant | Trigger | +|---------|---------| +| `BadMagic` | First byte is not `0x56` | +| `VarintOverflow` | LEB128 continuation bytes exceed MAX_BYTES (37) | +| `Truncated` | Buffer ends before a TLV value is fully read | +| `CompressionFailed` | Brotli decompression error on a wire payload | +| `UnsupportedVersion` | Version byte signals an unknown codec version | +| `DictionaryMismatch` | Dict hash in payload does not match compiled dict | +| `InvalidAmount` | Amount string exceeds U256::MAX or is not a valid decimal | + +See `src/error.rs` for the full 10-variant enum. diff --git a/packages/codec/docs/spike-bigint-boundary-2026-05.md b/packages/codec/docs/spike-bigint-boundary-2026-05.md new file mode 100644 index 0000000..77e8696 --- /dev/null +++ b/packages/codec/docs/spike-bigint-boundary-2026-05.md @@ -0,0 +1,75 @@ +--- +date: 2026-05-19 +task: T-P2-0b +spec: 056-void-layer-codec-extraction +decision: D-B11 +status: AMENDED +--- + +# Spike: BigInt WASM↔JS Boundary Failure Modes + +**Goal**: Validate D-B11 — observe what actually happens when Rust serializes `u64`/`u128` to JS via +`serde-wasm-bindgen` 0.6 with and without `.serialize_large_number_types_as_bigints(true)`. + +**Setup**: `wasm-pack test --node` (wasm-pack 0.13.1, Rust 1.85.0, serde-wasm-bindgen 0.6.5) + +--- + +## Config-A-vs-B Failure-Mode Table + +| Field | Config A type | Config A behavior | Config B type | Config B value | +|-------|--------------|-------------------|--------------|----------------| +| `u64::MAX` (18446744073709551615) | `Err` | serialization error: "can't be represented as a JavaScript number" | `bigint` | `18446744073709551615n` (exact) | +| `above_2_53` (9007199254740993) | `Err` | serialization error: "can't be represented as a JavaScript number" | `bigint` | `9007199254740993n` (exact) | +| `safe_53` (9007199254740992 = 2^53) | `Err` | serialization error: "can't be represented as a JavaScript number" | `bigint` | `9007199254740992n` (exact) | +| `string_amount` (uint256::MAX decimal) | `string` | round-trips intact | `string` | round-trips intact | + +**Key observation**: Config A does NOT silently truncate. It hard-errors on ALL `u64` values +(including values that fit in f64 mantissa). This is stricter than predicted. + +--- + +## Spec §4.8 Prediction vs. Actual + +| Prediction (§4.8) | Actual (serde-wasm-bindgen 0.6) | +|---|---| +| Config A silently truncates u64 >2^53 to f64 | Config A returns `Err` for ALL u64, including safe values | +| Config A values ≤2^53 are exact JS Numbers | Config A returns `Err` even for 2^53 | +| Config B yields `bigint` | CONFIRMED — Config B yields `bigint`, exact | + +--- + +## Zod `.refine()` Verification + +Mirrors TS: `z.string().refine(v => { try { BigInt(v); return true; } catch { return false; } })` + +Tested via `js_sys::BigInt::new(&JsValue::from_str(s))` in wasm-bindgen-test Node runner: + +| Input | Expected | Actual | +|-------|----------|--------| +| `"0"` | ACCEPT | PASS — `BigInt("0")` succeeds | +| `"1"` | ACCEPT | PASS — `BigInt("1")` succeeds | +| uint256::MAX decimal string | ACCEPT | PASS — `BigInt("<78-digit string>")` succeeds | +| `"1e18"` | REJECT | PASS — `BigInt("1e18")` throws | +| `"abc"` | REJECT | PASS — `BigInt("abc")` throws | +| `"1.5"` | REJECT | PASS — `BigInt("1.5")` throws | + +All 6 cases confirmed. The Zod refine strategy is sound. + +--- + +## VERDICT + +**D-B11 AMENDED**: The spec §4.8 prediction that "Config A silently truncates u64/u128 to f64 for +values >2^53" is incorrect for serde-wasm-bindgen 0.6. The actual behavior is that the default +serializer hard-errors (`Err`) on ALL `u64` values — it does not produce a JS Number at all. + +Consequence for codec design: +1. `.serialize_large_number_types_as_bigints(true)` is **mandatory** to successfully serialize any + `u64`/`u128` across the WASM boundary (not just for large values — for all u64). +2. The string-amount path (decimal string) is safe under BOTH configs and remains the preferred + approach for invoice amounts — immune to this failure mode entirely. +3. The Zod `.refine(v => BigInt(v))` guard on the TS consumer side is confirmed correct for + validating incoming decimal-string amounts (rejects scientific notation, decimals, non-numeric). + +Regression test: `packages/codec/tests/bigint_boundary.rs` — 10/10 green under `wasm-pack test --node`. diff --git a/packages/codec/docs/spike-brotli-2026-05.md b/packages/codec/docs/spike-brotli-2026-05.md new file mode 100644 index 0000000..7c0a2c9 --- /dev/null +++ b/packages/codec/docs/spike-brotli-2026-05.md @@ -0,0 +1,195 @@ +--- +task: T-P2-0a +date: 2026-05-19 +corpus: synthetic-content +remeasure_trigger: "Re-run against real /history export before v1.2 ships — synthetic corpus cannot capture real-world text-field diversity." +spec: 056-void-layer-codec-extraction §3.16 + §D-R6 +authored_by: exec.atlas-dev +--- + +# Brotli Spike — Compression + WASM Blob Measurement + +## Context + +Phase 2 pre-implementation spike (T-P2-0a). Goal: validate which Brotli variant fits the 200 KB +total-package cap (D-B9) and whether compressed invoice payloads exceed 400 B median (Plan-C +trigger). Ignat pre-decision: B-iv (decode-only Rust; encode via native JS `CompressionStream`). +This spike provides the evidentiary record. + +**Corpus**: synthetic-content — 20 invoice objects generated from the vl/app TS reference codec. +Real-format TLV bytes, varied shape (1–3 line items, with/without notes/clientAddress, 5 EVM +networks). NOT generic web text. Compression ratios are defensible but conservative; re-measure +with real `/history` export before v1.2 ships. + +--- + +## §1 — Corpus Summary (Step 1) + +20 invoices generated via `packages/codec/scripts/generate-spike-corpus.ts` → +`packages/codec/vectors/spike-corpus/`. + +| Shape | Uncompressed (B) | +|----------------------------------|-----------------| +| minimal-1item-evm | 143 | +| medium-2items-evm-notes | 243 | +| full-3items-evm-all-fields | 488 | +| minimal-1item-eth-mainnet | 145 | +| minimal-1item-polygon | 144 | +| minimal-1item-base | 150 | +| minimal-1item-optimism | 140 | +| medium-2items-usdc-arb | 187 | +| medium-2items-no-notes | 174 | +| full-3items-client-wallet | 258 | +| full-3items-tax-discount | 209 | +| medium-2items-long-descriptions | 428 | +| minimal-1item-raw-currency | 168 | +| full-3items-all-optional-text | 564 | +| minimal-1item-small-amount | 143 | +| minimal-1item-large-amount | 176 | +| medium-2items-fractional-qty | 193 | +| full-3items-eip712-heavy | 376 | +| medium-2items-long-invoiceid | 241 | +| full-3items-both-emails | 327 | + +**Statistics**: Min 140 B · Max 564 B · Median 193 B + +--- + +## §2 — Compression Ratio Table (Step 2) + +Measured on 20-invoice corpus. "Native deflate-raw" column uses `CompressionStream('deflate-raw')` +in Bun 1.3.5 (Bun does NOT support `'brotli'` in CompressionStream — native Brotli is browser-only +via `CompressionStream`; this column is a deflate reference). "brotli-wasm" uses `brotli-wasm@3` +at quality=11, matching production settings. + +| Payload | Uncompressed (B) | Native deflate-raw (B) | brotli-wasm q=11 (B) | +|----------------------------------|-----------------|------------------------|----------------------| +| minimal-1item-evm | 143 | 135 | 147 | +| medium-2items-evm-notes | 243 | 231 | 227 | +| full-3items-evm-all-fields | 488 | 444 | 368 | +| minimal-1item-eth-mainnet | 145 | 137 | 149 | +| minimal-1item-polygon | 144 | 135 | 148 | +| minimal-1item-base | 150 | 141 | 154 | +| minimal-1item-optimism | 140 | 132 | 144 | +| medium-2items-usdc-arb | 187 | 179 | 185 | +| medium-2items-no-notes | 174 | 163 | 170 | +| full-3items-client-wallet | 258 | 243 | 232 | +| full-3items-tax-discount | 209 | 201 | 187 | +| medium-2items-long-descriptions | 428 | 378 | 307 | +| minimal-1item-raw-currency | 168 | 154 | 172 | +| full-3items-all-optional-text | 564 | 504 | 447 | +| minimal-1item-small-amount | 143 | 135 | 147 | +| minimal-1item-large-amount | 176 | 167 | 176 | +| medium-2items-fractional-qty | 193 | 180 | 175 | +| full-3items-eip712-heavy | 376 | 339 | 318 | +| medium-2items-long-invoiceid | 241 | 230 | 210 | +| full-3items-both-emails | 327 | 302 | 310 | + +**Median compressed (brotli-wasm q=11)**: 185 B +**Plan-C trigger check**: Median 185 B < 400 B threshold → Plan-C (Zstd+SHA-256-dict) NOT triggered. + +**Observation**: Brotli expands small payloads (<180 B) vs raw; this is expected for Brotli on +tiny inputs. The whole-payload Brotli in `compressPayload()` already handles this with a fallback +(returns uncompressed if `compressed.length >= body.length`). No action needed. + +--- + +## §3 — WASM Blob Measurement (Step 3) + +Measured on throwaway branches from the Phase 1 hello-world lib (`lib.rs` with a minimal +`#[wasm_bindgen]` export forcing linker inclusion of the dep). Build chain: +`cargo build --release --target wasm32-unknown-unknown` → `wasm-pack build --target bundler +--release` (wasm-pack 0.13.1, wasm-opt at /usr/local/bin/wasm-opt, profile: opt-z + lto=fat + +strip=symbols). Toolchain: Rust 1.85.0. + +### Variant A — B-iv baseline (brotli-decompressor decoder-only) + +Cargo.toml dep: `brotli-decompressor = "4"` + `wasm-bindgen = "0.2"` + +Probe: `spike_decompress(data: &[u8]) -> Vec` using `brotli_decompressor::Decompressor`. + +| Metric | Value | +|---------------------------------|------------| +| WASM blob (wasm-opt -Oz) | 200,921 B | +| WASM blob (KB) | ~196 KB | +| pkg/ total uncompressed | 205,725 B | +| pkg/ total uncompressed (KB) | ~201 KB | +| pkg/ tarball gzip (publish size)| ~100 KB | + +**Assessment**: WASM blob ~196 KB. pkg/ total ~201 KB — **marginally over** the 200 KB cap by 1 KB. +This is with the minimal Phase 1 scaffold; the Phase 2 production build will add more exports +(encode, decode, compute_content_hash) which may add a few KB. The 80 KB wasm sub-cap is flagged +under review per dispatch brief — not treated as hard fail. The 200 KB cap is a soft design +target; Ignat should confirm tolerance at T-P2-1. + +Note: the pre-decision reference figure of ~137.8 KB for B-iv was from a different measurement +context (possibly smaller probe or different opt settings). Measured figure is ~196 KB with this +spike probe. + +### Variant B — B-i candidate (full brotli encoder+decoder) + +Cargo.toml dep: `brotli = { version = "7", default-features = false, features = ["std"] }` + +`wasm-bindgen = "0.2"` + +Probe: both `spike_compress` (encoder) and `spike_decompress` (decoder). + +| Metric | Value | +|---------------------------------|------------| +| WASM blob (wasm-opt -Oz) | 976,050 B | +| WASM blob (KB) | ~953 KB | +| pkg/ total uncompressed | 982,063 B | +| pkg/ total uncompressed (KB) | ~959 KB | +| pkg/ tarball gzip (publish size)| ~470 KB | + +**Assessment**: B-i RULED OUT. Full brotli crate is ~953 KB wasm blob — 4.8× over the 200 KB cap. + +### Variant C — brotli v7 no-stdlib (attempted) + +Cargo.toml dep: `brotli = { version = "7", default-features = false, features = [] }` + +**Result**: Compilation failure — `brotli` v7 without `std` feature exposes no usable +decompress-only API surface (the `std` feature only gates alloc-stdlib + IO wrappers, NOT the +encoder itself). There is no decoder-only feature flag in `brotli` v7. The encoder is always +linked regardless of `features = []`. Variant C ≈ Variant B (~953 KB) and was not fully built. + +**Confirmed by Cargo.toml feature inspection**: `brotli` v7 features are `std`, `billing`, +`benchmark`, `simd`, `float64`, etc. — none are `decoder-only` or `encoder-only`. + +--- + +## §4 — VERDICT + +> **B-i RULED OUT** — full `brotli` crate produces ~953 KB WASM blob; no decoder-only feature +> gate exists in brotli v7; Variant C ≈ Variant B. +> +> **B-iv CONFIRMED** — `brotli-decompressor = "4"` decoder-only produces ~196 KB WASM blob / +> ~201 KB pkg total. Matches Ignat pre-decision. Encode-wire is native JS-side via +> `CompressionStream('deflate')` or `brotli-wasm` in the consumer layer. Rust ships only the +> decompressor. +> +> **Cap note**: pkg/ total of ~201 KB is 1 KB over the 200 KB design cap — within measurement +> noise. The 80 KB wasm sub-cap is under review. Confirm tolerance at T-P2-1 before finalizing +> the `brotli-decompressor = "4"` dep entry. +> +> **Plan-C NOT triggered**: Median compressed payload 185 B < 400 B threshold. + +--- + +## §5 — Follow-up Actions + +| Item | Owner | When | +|------|-------|------| +| Confirm 200 KB cap tolerance (~201 KB measured) | Ignat / Kai | T-P2-1 | +| Re-measure with real `/history` export | Atlas | Before v1.2 ship | +| Wire `brotli-decompressor = "4"` dep permanently | Atlas (T-P2-1) | After T-P2-0b verdict | +| Investigate ~137.8 KB reference figure discrepancy | Kai | Advisory only | + +--- + +## §6 — Tooling Notes + +- wasm-pack 0.13.1 installed via `cargo install wasm-pack --version 0.13.1 --locked` + (latest wasm-pack 0.15.0 requires Rust 1.86+; project toolchain is 1.85.0) +- wasm-opt found at `/usr/local/bin/wasm-opt` (pre-installed) +- Corpus runner: `bun run` from `/Users/ignat/code/vl/app` (brotli-wasm + path-alias resolution) +- Throwaway branches `spike/brotli-A-readonly` and `spike/brotli-B-full` deleted after measurement diff --git a/packages/codec/package.json b/packages/codec/package.json new file mode 100644 index 0000000..b684a45 --- /dev/null +++ b/packages/codec/package.json @@ -0,0 +1,55 @@ +{ + "name": "@void-layer/codec", + "version": "0.1.0", + "description": "Canonical Invoice codec — TLV + Brotli wire format. v1 schema LOCKED.", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist/", + "pkg/", + "README.md", + "LICENSE", + "REGISTRY.md" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/void-layer/codec.git", + "directory": "packages/codec" + }, + "license": "MIT", + "scripts": { + "build": "wasm-pack build --target bundler --release --out-dir pkg && rm -f pkg/.gitignore && tsc", + "build:wasm": "wasm-pack build --target bundler --release --out-dir pkg && rm -f pkg/.gitignore", + "build:web": "wasm-pack build --target web --release --out-dir pkg-web", + "build:nodejs": "wasm-pack build --target nodejs --release --out-dir pkg-node", + "test": "vitest run", + "test:rust": "cargo test --manifest-path Cargo.toml", + "test:wasm": "wasm-pack test --node", + "generate-vectors": "vitest run --config scripts/generate-vectors.config.ts", + "lint": "cargo clippy --all-targets --all-features --locked -- -D warnings && cargo fmt --manifest-path Cargo.toml -- --check", + "size": "ls -la pkg/void_layer_codec_bg.wasm" + }, + "peerDependencies": { + "brotli-wasm": "^3.0.1" + }, + "devDependencies": { + "@types/node": "25.9.1", + "@vitest/coverage-v8": "3.2.4", + "brotli-wasm": "^3.0.1", + "typescript": "^6.0.3", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.4.1", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=24" + } +} diff --git a/packages/codec/rust-toolchain.toml b/packages/codec/rust-toolchain.toml new file mode 100644 index 0000000..daea42a --- /dev/null +++ b/packages/codec/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "1.85.0" +components = ["rustfmt", "clippy", "rust-src"] +targets = ["wasm32-unknown-unknown"] +profile = "minimal" diff --git a/packages/codec/rustfmt.toml b/packages/codec/rustfmt.toml new file mode 100644 index 0000000..710f8c5 --- /dev/null +++ b/packages/codec/rustfmt.toml @@ -0,0 +1,7 @@ +edition = "2024" +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Unix" +use_field_init_shorthand = true +use_try_shorthand = true diff --git a/packages/codec/scripts/assert-size.sh b/packages/codec/scripts/assert-size.sh new file mode 100755 index 0000000..af09eb4 --- /dev/null +++ b/packages/codec/scripts/assert-size.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail +WASM_PATH="${WASM_PATH:-pkg/void_layer_codec_bg.wasm}" +MAX_WASM_GZIP_BYTES=81920 # 80 KB GZIPPED — spec §3 (B-v) +MAX_PACKAGE_BYTES=204800 # 200 KB tarball +gzip_wasm=$(gzip -c "$WASM_PATH" | wc -c) +echo "WASM gzip: ${gzip_wasm} bytes (cap: ${MAX_WASM_GZIP_BYTES})" +[[ "$gzip_wasm" -le "$MAX_WASM_GZIP_BYTES" ]] || { echo "FAIL: wasm gzip exceeds cap"; exit 1; } +actual_pkg=$(tar czf - pkg/ dist/ | wc -c) +echo "Package tarball: ${actual_pkg} bytes (cap: ${MAX_PACKAGE_BYTES})" +[[ "$actual_pkg" -le "$MAX_PACKAGE_BYTES" ]] || { echo "FAIL: package exceeds cap"; exit 1; } +echo "OK" diff --git a/packages/codec/scripts/generate-spike-corpus.ts b/packages/codec/scripts/generate-spike-corpus.ts new file mode 100644 index 0000000..0bf25c0 --- /dev/null +++ b/packages/codec/scripts/generate-spike-corpus.ts @@ -0,0 +1,600 @@ +/** + * Brotli Spike Corpus Generator + * + * Generates 20+ synthetic invoice objects using the vl/app TS reference codec, + * encodes each to TLV wire bytes (uncompressed), and writes one JSON file per + * invoice to vectors/spike-corpus/. + * + * Usage (run from /Users/ignat/code/vl/app): + * npx tsx --tsconfig tsconfig.json \ + * /Users/ignat/code/vl/codec/packages/codec/scripts/generate-spike-corpus.ts + * + * Each output JSON: + * { source, generated_at, bytes_hex, uncompressed_length, shape } + * + * spike_id: brotli-2026-05 + */ + +import { writeTlv, sortCanonical, writeVarInt, writeMantissa, writeQuantity } from '/Users/ignat/code/vl/app/src/shared/lib/tlv-codec' +import type { TlvRecord } from '/Users/ignat/code/vl/app/src/shared/lib/tlv-codec' +import type { Invoice } from '/Users/ignat/code/vl/app/src/shared/lib/invoice-types' +import { applyDict } from '/Users/ignat/code/vl/app/src/features/invoice-codec/lib/app-dict' +import { encodeChainId } from '/Users/ignat/code/vl/app/src/features/invoice-codec/lib/chain-dict' +import { TlvType, encodeCurrency, encodeTokenAddress } from '/Users/ignat/code/vl/app/src/features/invoice-codec/lib/tlv-map' +import { generateSalt, computeDomainSeparator } from '/Users/ignat/code/vl/app/src/features/invoice-codec/lib/security' +import * as fs from 'node:fs' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +// __dirname unavailable in ESM — derive from import.meta.url +const _filename = fileURLToPath(import.meta.url) +const _dirname = path.dirname(_filename) + +const CORPUS_DIR = path.resolve(_dirname, '../vectors/spike-corpus') +const NOW_UNIX = Math.floor(Date.now() / 1000) +const ONE_DAY = 86400 + +// ---- helpers (mirrors encode.ts without brotli/base64url) ------------------ + +function utf8(s: string): Uint8Array { + return new TextEncoder().encode(s) +} + +function addressToBytes(address: string): Uint8Array { + const hex = address.startsWith('0x') ? address.slice(2) : address + const bytes = new Uint8Array(20) + for (let i = 0; i < 20; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16) + return bytes +} + +function uint32BE(value: number): Uint8Array { + const b = new Uint8Array(4) + b[0] = (value >>> 24) & 0xff; b[1] = (value >>> 16) & 0xff + b[2] = (value >>> 8) & 0xff; b[3] = value & 0xff + return b +} + +function varintBytes(value: number): Uint8Array { + const buf: number[] = []; writeVarInt(buf, value); return new Uint8Array(buf) +} + +function mantissaBytes(value: bigint): Uint8Array { + const buf: number[] = []; writeMantissa(buf, value); return new Uint8Array(buf) +} + +function packItems(items: Invoice['items']): Uint8Array { + const buf: number[] = [] + writeVarInt(buf, items.length) + for (const item of items) { + const descBytes = applyDict(utf8(item.description)) + writeVarInt(buf, descBytes.length) + for (let i = 0; i < descBytes.length; i++) buf.push(descBytes[i]!) + writeQuantity(buf, item.quantity) + writeMantissa(buf, BigInt(item.rate || '0')) + } + return new Uint8Array(buf) +} + +/** + * Encode invoice to raw TLV bytes (no compression, no base64url). + * Mirrors encode.ts buildRecords logic exactly. + */ +function encodeToTlvBytes(invoice: Invoice, salt: Uint8Array): Uint8Array { + const records: TlvRecord[] = [] + + const chainBuf: number[] = [] + encodeChainId(chainBuf, invoice.networkId) + records.push({ type: TlvType.CHAIN_ID, value: new Uint8Array(chainBuf) }) + records.push({ type: TlvType.ISSUED_AT, value: uint32BE(invoice.issuedAt) }) + records.push({ type: TlvType.DUE_AT, value: varintBytes(invoice.dueAt - invoice.issuedAt) }) + records.push({ type: TlvType.DECIMALS, value: new Uint8Array([invoice.decimals]) }) + records.push({ type: TlvType.FROM_WALLET, value: addressToBytes(invoice.from.walletAddress) }) + + const currCode = encodeCurrency(invoice.currency) + if (currCode !== null) { + records.push({ type: TlvType.CURRENCY, value: new Uint8Array([0x00, currCode]) }) + } else { + const rawCurr = utf8(invoice.currency) + const val = new Uint8Array(1 + rawCurr.length) + val[0] = 0x01; val.set(rawCurr, 1) + records.push({ type: TlvType.CURRENCY, value: val }) + } + + records.push({ type: TlvType.ITEMS, value: packItems(invoice.items) }) + records.push({ type: TlvType.INVOICE_ID, value: utf8(invoice.invoiceId) }) + records.push({ type: TlvType.SALT, value: salt }) + records.push({ type: TlvType.FROM_NAME, value: applyDict(utf8(invoice.from.name)) }) + records.push({ type: TlvType.CLIENT_NAME, value: applyDict(utf8(invoice.client.name)) }) + + if (invoice.notes) records.push({ type: TlvType.NOTES, value: applyDict(utf8(invoice.notes)) }) + if (invoice.from.email) records.push({ type: TlvType.FROM_EMAIL, value: applyDict(utf8(invoice.from.email)) }) + if (invoice.from.phone) records.push({ type: TlvType.FROM_PHONE, value: applyDict(utf8(invoice.from.phone)) }) + if (invoice.from.physicalAddress) records.push({ type: TlvType.FROM_ADDRESS, value: applyDict(utf8(invoice.from.physicalAddress)) }) + if (invoice.from.taxId) records.push({ type: TlvType.FROM_TAX_ID, value: applyDict(utf8(invoice.from.taxId)) }) + if (invoice.client.email) records.push({ type: TlvType.CLIENT_EMAIL, value: applyDict(utf8(invoice.client.email)) }) + if (invoice.client.phone) records.push({ type: TlvType.CLIENT_PHONE, value: applyDict(utf8(invoice.client.phone)) }) + if (invoice.client.physicalAddress) records.push({ type: TlvType.CLIENT_ADDRESS, value: applyDict(utf8(invoice.client.physicalAddress)) }) + if (invoice.client.taxId) records.push({ type: TlvType.CLIENT_TAX_ID, value: applyDict(utf8(invoice.client.taxId)) }) + + if (invoice.tokenAddress) { + const tokenEntry = encodeTokenAddress(invoice.tokenAddress, invoice.networkId) + if (tokenEntry) { + records.push({ type: TlvType.TOKEN_ADDRESS, value: new Uint8Array([0x00, tokenEntry.code]) }) + } else { + const rawAddr = addressToBytes(invoice.tokenAddress) + const val = new Uint8Array(1 + 20); val[0] = 0x01; val.set(rawAddr, 1) + records.push({ type: TlvType.TOKEN_ADDRESS, value: val }) + } + } + + if (invoice.client.walletAddress) { + records.push({ type: TlvType.CLIENT_WALLET, value: addressToBytes(invoice.client.walletAddress) }) + } + if (invoice.tax) records.push({ type: TlvType.TAX, value: utf8(invoice.tax) }) + if (invoice.discount) records.push({ type: TlvType.DISCOUNT, value: utf8(invoice.discount) }) + + const total = BigInt(invoice.total ?? '0') + records.push({ type: TlvType.TOTAL, value: mantissaBytes(total) }) + + const sorted = sortCanonical(records) + const domainSep = computeDomainSeparator(sorted) + sorted.push({ type: TlvType.DOMAIN_SEPARATOR, value: domainSep }) + const finalRecords = sortCanonical(sorted) + + return writeTlv(finalRecords) +} + +// ---- invoice fixtures ------------------------------------------------------- + +type Shape = + | 'minimal-1item-evm' + | 'medium-2items-evm-notes' + | 'full-3items-evm-all-fields' + | 'minimal-1item-eth-mainnet' + | 'minimal-1item-polygon' + | 'minimal-1item-base' + | 'minimal-1item-optimism' + | 'medium-2items-usdc-arb' + | 'medium-2items-no-notes' + | 'full-3items-client-wallet' + | 'full-3items-tax-discount' + | 'medium-2items-long-descriptions' + | 'minimal-1item-raw-currency' + | 'full-3items-all-optional-text' + | 'minimal-1item-small-amount' + | 'minimal-1item-large-amount' + | 'medium-2items-fractional-qty' + | 'full-3items-eip712-heavy' + | 'medium-2items-long-invoiceid' + | 'full-3items-both-emails' + +interface CorpusEntry { + source: 'synthetic-via-ts-codec' + generated_at: string + bytes_hex: string + uncompressed_length: number + shape: Shape +} + +const SALT_FIXED = new Uint8Array(16).fill(0x42) // deterministic for audit + +const FROM_ETH = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as const +const CLIENT_ETH = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' as const + +function makeInvoice(overrides: Partial & Pick): Invoice { + return { + issuedAt: NOW_UNIX, + dueAt: NOW_UNIX + 30 * ONE_DAY, + ...overrides, + } +} + +const fixtures: Array<{ shape: Shape; invoice: Invoice }> = [ + { + shape: 'minimal-1item-evm', + invoice: makeInvoice({ + invoiceId: 'INV-001', + networkId: 42161, // Arbitrum + currency: 'USDC', + decimals: 6, + total: '1250000000', + from: { name: 'Alice', walletAddress: FROM_ETH }, + client: { name: 'Bob' }, + items: [{ description: 'Consulting', quantity: 1, rate: '1250000000' }], + }), + }, + { + shape: 'medium-2items-evm-notes', + invoice: makeInvoice({ + invoiceId: 'INV-002', + networkId: 42161, + currency: 'USDC', + decimals: 6, + total: '3500000000', + notes: 'Net 30 payment terms. Thank you for your business.', + from: { name: 'Alice Dev Studio', walletAddress: FROM_ETH, email: 'alice@example.com' }, + client: { name: 'Acme Corp' }, + items: [ + { description: 'Backend development', quantity: 20, rate: '150000000' }, + { description: 'Code review', quantity: 5, rate: '100000000' }, + ], + }), + }, + { + shape: 'full-3items-evm-all-fields', + invoice: makeInvoice({ + invoiceId: 'INV-003-FULL', + networkId: 1, // Ethereum mainnet + currency: 'USDC', + decimals: 6, + total: '5600000000', + notes: 'Please include invoice number in payment reference. VAT registered business.', + from: { + name: 'Alice Dev Studio Ltd', + walletAddress: FROM_ETH, + email: 'billing@alicedev.io', + phone: '+1-555-0100', + physicalAddress: '123 Main St, San Francisco, CA 94105', + taxId: 'US-TAX-123456', + }, + client: { + name: 'Acme Corporation', + walletAddress: CLIENT_ETH, + email: 'ap@acme.com', + phone: '+1-555-0200', + physicalAddress: '456 Corp Ave, New York, NY 10001', + taxId: 'US-TAX-789012', + }, + items: [ + { description: 'Smart contract audit', quantity: 1, rate: '3000000000' }, + { description: 'Frontend development', quantity: 16, rate: '150000000' }, + { description: 'Technical documentation', quantity: 8, rate: '100000000' }, + ], + }), + }, + { + shape: 'minimal-1item-eth-mainnet', + invoice: makeInvoice({ + invoiceId: 'INV-004', + networkId: 1, + currency: 'ETH', + decimals: 18, + total: '1000000000000000000', + from: { name: 'Carol', walletAddress: FROM_ETH }, + client: { name: 'Dave' }, + items: [{ description: 'Design work', quantity: 1, rate: '1000000000000000000' }], + }), + }, + { + shape: 'minimal-1item-polygon', + invoice: makeInvoice({ + invoiceId: 'INV-005', + networkId: 137, + currency: 'USDC', + decimals: 6, + total: '500000000', + from: { name: 'Eve', walletAddress: FROM_ETH }, + client: { name: 'Frank' }, + items: [{ description: 'Logo design', quantity: 1, rate: '500000000' }], + }), + }, + { + shape: 'minimal-1item-base', + invoice: makeInvoice({ + invoiceId: 'INV-006', + networkId: 8453, + currency: 'USDC', + decimals: 6, + total: '750000000', + from: { name: 'Grace', walletAddress: FROM_ETH }, + client: { name: 'Henry' }, + items: [{ description: 'API integration', quantity: 1, rate: '750000000' }], + }), + }, + { + shape: 'minimal-1item-optimism', + invoice: makeInvoice({ + invoiceId: 'INV-007', + networkId: 10, + currency: 'USDC', + decimals: 6, + total: '200000000', + from: { name: 'Iris', walletAddress: FROM_ETH }, + client: { name: 'Jack' }, + items: [{ description: 'Bug fix', quantity: 2, rate: '100000000' }], + }), + }, + { + shape: 'medium-2items-usdc-arb', + invoice: makeInvoice({ + invoiceId: 'INV-008', + networkId: 42161, + currency: 'USDC', + decimals: 6, + total: '2250000000', + from: { name: 'Karl Blockchain', walletAddress: FROM_ETH }, + client: { name: 'Luna Protocol' }, + items: [ + { description: 'DeFi integration', quantity: 10, rate: '200000000' }, + { description: 'Testing & QA', quantity: 5, rate: '50000000' }, + ], + }), + }, + { + shape: 'medium-2items-no-notes', + invoice: makeInvoice({ + invoiceId: 'INV-009', + networkId: 42161, + currency: 'DAI', + decimals: 18, + total: '1800000000000000000000', + from: { name: 'Mia Studio', walletAddress: FROM_ETH }, + client: { name: 'Nova Corp' }, + items: [ + { description: 'UI/UX design', quantity: 12, rate: '100000000000000000000' }, + { description: 'Design system', quantity: 6, rate: '100000000000000000000' }, + ], + }), + }, + { + shape: 'full-3items-client-wallet', + invoice: makeInvoice({ + invoiceId: 'INV-010', + networkId: 1, + currency: 'USDC', + decimals: 6, + total: '4500000000', + from: { name: 'Oscar Dev', walletAddress: FROM_ETH, email: 'oscar@dev.io' }, + client: { name: 'Pam Finance', walletAddress: CLIENT_ETH, email: 'pam@finance.io' }, + items: [ + { description: 'Architecture review', quantity: 1, rate: '2000000000' }, + { description: 'Implementation', quantity: 20, rate: '100000000' }, + { description: 'Deployment support', quantity: 5, rate: '100000000' }, + ], + }), + }, + { + shape: 'full-3items-tax-discount', + invoice: makeInvoice({ + invoiceId: 'INV-011', + networkId: 42161, + currency: 'USDC', + decimals: 6, + total: '4720000000', + tax: '10', + discount: '5', + from: { name: 'Quinn Agency', walletAddress: FROM_ETH }, + client: { name: 'Ross Industries' }, + items: [ + { description: 'Strategy consulting', quantity: 1, rate: '2000000000' }, + { description: 'Market research', quantity: 1, rate: '1500000000' }, + { description: 'Report writing', quantity: 1, rate: '500000000' }, + ], + }), + }, + { + shape: 'medium-2items-long-descriptions', + invoice: makeInvoice({ + invoiceId: 'INV-012', + networkId: 42161, + currency: 'USDC', + decimals: 6, + total: '6000000000', + notes: 'Extended engagement for Q2 2026 product development sprint covering all milestones.', + from: { name: 'Sam Engineering', walletAddress: FROM_ETH }, + client: { name: 'Terra Startup' }, + items: [ + { + description: 'Full-stack web application development including backend API, database schema design, and React frontend', + quantity: 1, + rate: '4000000000', + }, + { + description: 'CI/CD pipeline setup, Docker containerization, AWS deployment, monitoring and alerting configuration', + quantity: 1, + rate: '2000000000', + }, + ], + }), + }, + { + shape: 'minimal-1item-raw-currency', + invoice: makeInvoice({ + invoiceId: 'INV-013', + networkId: 42161, + currency: 'WBTC', + decimals: 8, + total: '1000000', + from: { name: 'Uma Bitcoin', walletAddress: FROM_ETH }, + client: { name: 'Victor Fund' }, + items: [{ description: 'Bitcoin custody setup', quantity: 1, rate: '1000000' }], + }), + }, + { + shape: 'full-3items-all-optional-text', + invoice: makeInvoice({ + invoiceId: 'INV-014-LONG-ID-FOR-TESTING', + networkId: 1, + currency: 'USDC', + decimals: 6, + total: '9500000000', + notes: 'Payment due within 30 days. Late fees of 1.5% per month apply after due date.', + from: { + name: 'Wendy Tech Solutions', + walletAddress: FROM_ETH, + email: 'wendy@techsolutions.io', + phone: '+44-20-7946-0958', + physicalAddress: '10 Downing St, London, UK SW1A 2AA', + taxId: 'GB-VAT-123456789', + }, + client: { + name: 'Xavier Enterprises', + walletAddress: CLIENT_ETH, + email: 'xavier@enterprises.com', + phone: '+1-212-555-0150', + physicalAddress: '1 World Trade Center, New York, NY 10007', + taxId: 'US-EIN-12-3456789', + }, + items: [ + { description: 'Enterprise software license', quantity: 1, rate: '5000000000' }, + { description: 'Implementation & onboarding', quantity: 1, rate: '3000000000' }, + { description: 'First year support contract', quantity: 1, rate: '1500000000' }, + ], + }), + }, + { + shape: 'minimal-1item-small-amount', + invoice: makeInvoice({ + invoiceId: 'INV-015', + networkId: 137, + currency: 'USDC', + decimals: 6, + total: '5000000', + from: { name: 'Yara', walletAddress: FROM_ETH }, + client: { name: 'Zoe' }, + items: [{ description: 'Translation', quantity: 1, rate: '5000000' }], + }), + }, + { + shape: 'minimal-1item-large-amount', + invoice: makeInvoice({ + invoiceId: 'INV-016', + networkId: 1, + currency: 'USDC', + decimals: 6, + total: '500000000000', + from: { name: 'Atlas Capital', walletAddress: FROM_ETH }, + client: { name: 'Nexus DAO' }, + items: [{ description: 'Protocol acquisition advisory', quantity: 1, rate: '500000000000' }], + }), + }, + { + shape: 'medium-2items-fractional-qty', + invoice: makeInvoice({ + invoiceId: 'INV-017', + networkId: 8453, + currency: 'USDC', + decimals: 6, + total: '875000000', + from: { name: 'Blake Design', walletAddress: FROM_ETH }, + client: { name: 'Cyan Media' }, + items: [ + { description: 'Brand identity design', quantity: 1.5, rate: '400000000' }, + { description: 'Social media assets', quantity: 2.5, rate: '70000000' }, + ], + }), + }, + { + shape: 'full-3items-eip712-heavy', + invoice: makeInvoice({ + invoiceId: 'INV-018-EIP712', + networkId: 42161, + currency: 'USDC', + decimals: 6, + total: '7300000000', + notes: 'EIP-712 signed invoice for on-chain payment verification.', + from: { + name: 'Drew Protocol Labs', + walletAddress: FROM_ETH, + email: 'drew@protocollabs.xyz', + }, + client: { + name: 'Ember DAO Treasury', + walletAddress: CLIENT_ETH, + email: 'treasury@emberdao.xyz', + }, + items: [ + { description: 'Protocol design & tokenomics', quantity: 1, rate: '3000000000' }, + { description: 'Smart contract development', quantity: 1, rate: '3000000000' }, + { description: 'Security audit coordination', quantity: 1, rate: '1300000000' }, + ], + }), + }, + { + shape: 'medium-2items-long-invoiceid', + invoice: makeInvoice({ + invoiceId: 'INVOICE-2026-Q2-DEVELOPMENT-SPRINT-042', + networkId: 10, + currency: 'USDC', + decimals: 6, + total: '2600000000', + from: { name: 'Faye Studio', walletAddress: FROM_ETH }, + client: { name: 'Gale Ventures' }, + items: [ + { description: 'Sprint planning & execution', quantity: 1, rate: '2000000000' }, + { description: 'Retrospective & documentation', quantity: 1, rate: '600000000' }, + ], + }), + }, + { + shape: 'full-3items-both-emails', + invoice: makeInvoice({ + invoiceId: 'INV-020', + networkId: 42161, + currency: 'USDC', + decimals: 6, + total: '3350000000', + from: { + name: 'Hank Consulting', + walletAddress: FROM_ETH, + email: 'hank@consulting.dev', + taxId: 'DE-USt-123456789', + }, + client: { + name: 'Ivy Solutions GmbH', + walletAddress: CLIENT_ETH, + email: 'billing@ivy-solutions.de', + taxId: 'DE-USt-987654321', + }, + items: [ + { description: 'Web3 integration consulting', quantity: 15, rate: '150000000' }, + { description: 'Technical due diligence', quantity: 8, rate: '100000000' }, + { description: 'Workshop facilitation', quantity: 3, rate: '200000000' }, + ], + }), + }, +] + +// ---- main ------------------------------------------------------------------ + +async function main(): Promise { + fs.mkdirSync(CORPUS_DIR, { recursive: true }) + + let count = 0 + const summary: Array<{ shape: Shape; uncompressed_length: number; file: string }> = [] + + for (const { shape, invoice } of fixtures) { + const tlvBytes = encodeToTlvBytes(invoice, SALT_FIXED) + const entry: CorpusEntry = { + source: 'synthetic-via-ts-codec', + generated_at: new Date().toISOString(), + bytes_hex: Buffer.from(tlvBytes).toString('hex'), + uncompressed_length: tlvBytes.length, + shape, + } + + const filename = `${String(count + 1).padStart(2, '0')}-${shape}.json` + const filepath = path.join(CORPUS_DIR, filename) + fs.writeFileSync(filepath, JSON.stringify(entry, null, 2) + '\n') + summary.push({ shape, uncompressed_length: tlvBytes.length, file: filename }) + count++ + } + + console.log(`\nGenerated ${count} corpus entries to ${CORPUS_DIR}\n`) + console.log('Shape | Uncompressed (B)') + console.log('------------------------------------|------------------') + for (const s of summary) { + console.log(`${s.shape.padEnd(35)} | ${s.uncompressed_length}`) + } + + const sizes = summary.map((s) => s.uncompressed_length) + const min = Math.min(...sizes) + const max = Math.max(...sizes) + const median = sizes.sort((a, b) => a - b)[Math.floor(sizes.length / 2)]! + console.log(`\nMin: ${min} B Max: ${max} B Median: ${median} B`) +} + +main().catch((err) => { + console.error('Corpus generation failed:', err) + process.exit(1) +}) diff --git a/packages/codec/scripts/generate-vectors.config.ts b/packages/codec/scripts/generate-vectors.config.ts new file mode 100644 index 0000000..4c09a24 --- /dev/null +++ b/packages/codec/scripts/generate-vectors.config.ts @@ -0,0 +1,24 @@ +/** + * Vitest config for explicit vector generation only. + * Used by: pnpm generate-vectors + * Overrides the main vitest.config.ts exclude so scripts/** is included. + */ +import { defineConfig } from 'vitest/config' +import wasm from 'vite-plugin-wasm' +import topLevelAwait from 'vite-plugin-top-level-await' +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) + +export default defineConfig({ + plugins: [wasm(), topLevelAwait()], + test: { + environment: 'node', + include: ['scripts/run-generate-vectors.test.ts'], + }, + resolve: { + alias: { + 'brotli-wasm': require.resolve('brotli-wasm'), + }, + }, +}) diff --git a/packages/codec/scripts/generate-vectors.ts b/packages/codec/scripts/generate-vectors.ts new file mode 100644 index 0000000..78885bd --- /dev/null +++ b/packages/codec/scripts/generate-vectors.ts @@ -0,0 +1,427 @@ +/** + * Golden vector generator — @void-layer/codec v4-codec.json + * + * Produces the starter set of 16 canonical golden vectors per spec §D-R6.1 and + * plan-phase2c §T-P2-12 (C2 amendment: TypeScript generator, not Rust bin). + * + * Run (from packages/codec root): + * pnpm -C packages/codec exec vite-node scripts/generate-vectors.ts + * + * Imports canonical encode/decode from the nodejs-target pkg-node/ (synchronous + * CJS-style — no Vite plugin required). Wire encode/decode mirrors the JS shim + * in src/index.ts using the same brotli-wasm peerDep. + * + * C4 amendment: wire_hex == canonical_hex when Brotli would expand the payload + * (small invoices). Each non-malformed vector carries both hex fields regardless. + */ + +import * as fs from 'node:fs' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' +import { + encodeInvoiceCanonical, + decodeInvoiceCanonical, +} from '../pkg-node/void_layer_codec.js' +// brotli-wasm: resolve the Node-compatible entry via bare specifier. +// vitest.config.ts aliases 'brotli-wasm' → the CJS-friendly Node build. +import brotliWasmInit from 'brotli-wasm' + +const _filename = fileURLToPath(import.meta.url) +const _dirname = path.dirname(_filename) +const VECTORS_DIR = path.resolve(_dirname, '../vectors') +const OUT_PATH = path.join(VECTORS_DIR, 'v4-codec.json') + +const COMPRESSED_FLAG = 0x80 + +// --------------------------------------------------------------------------- +// Wire encode/decode — mirrors src/index.ts logic exactly +// --------------------------------------------------------------------------- + +async function wireEncode(invoice: unknown): Promise { + const brotli = await brotliWasmInit + const canonical: Uint8Array = encodeInvoiceCanonical(invoice) + if (canonical.length < 3) return canonical + const body = canonical.slice(2) + const compressed = brotli.compress(body, { quality: 11 }) + if (compressed.length >= body.length) return canonical + const result = new Uint8Array(2 + compressed.length) + result[0] = canonical[0]! + result[1] = canonical[1]! | COMPRESSED_FLAG + result.set(compressed, 2) + return result +} + +async function wireDecode(bytes: Uint8Array): Promise { + if (bytes.length < 3 || !(bytes[1]! & COMPRESSED_FLAG)) { + return decodeInvoiceCanonical(bytes) + } + const brotli = await brotliWasmInit + const decompressed = brotli.decompress(bytes.slice(2)) + const canonical = new Uint8Array(2 + decompressed.length) + canonical[0] = bytes[0]! + canonical[1] = bytes[1]! & 0x7f + canonical.set(decompressed, 2) + return decodeInvoiceCanonical(canonical) +} + +// --------------------------------------------------------------------------- +// Invoice fixtures +// --------------------------------------------------------------------------- + +const ISSUED_AT = 1_700_000_000 +const DUE_AT = 1_700_086_400 +const FROM_WALLET = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' +const CLIENT_WALLET = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' +const SALT = 'deadbeefdeadbeefdeadbeefdeadbeef' + +function base(overrides: Record): Record { + return { + invoice_id: 'INV-001', + issued_at: ISSUED_AT, + due_at: DUE_AT, + network_id: 1, + currency: 'USDC', + decimals: 6, + from: { name: 'Alice', wallet_address: FROM_WALLET }, + client: { name: 'Bob' }, + items: [{ description: 'Consulting', quantity: 1.0, rate: '1000000' }], + total: '1000000', + salt: SALT, + ...overrides, + } +} + +function toHex(bytes: Uint8Array): string { + return Buffer.from(bytes).toString('hex') +} + +function isCompressed(hex: string): boolean { + if (hex.length < 4) return false + return (parseInt(hex.slice(2, 4), 16) & COMPRESSED_FLAG) !== 0 +} + +interface NonMalformedVector { + name: string + canonical_hex: string + wire_hex: string + decoded: unknown + roundtrip: boolean + diagnostic: string +} + +interface MalformedVector { + name: string + canonical_hex?: string + wire_hex?: string + decoded?: unknown + diagnostic: string + expected_error: string +} + +type Vector = NonMalformedVector | MalformedVector + +const WIRE_DIAG = + 'wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)' + +async function nonMalformed( + name: string, + invoice: Record, + diagnostic?: string, +): Promise { + const canonical = encodeInvoiceCanonical(invoice) + const wire = await wireEncode(invoice) + const canonical_hex = toHex(canonical) + const wire_hex = toHex(wire) + const decodedC = decodeInvoiceCanonical(canonical) + const decodedW = await wireDecode(wire) + const roundtrip = JSON.stringify(decodedC) === JSON.stringify(decodedW) + return { + name, + canonical_hex, + wire_hex, + decoded: decodedC, + roundtrip, + diagnostic: diagnostic ?? WIRE_DIAG, + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + const vectors: Vector[] = [] + + // 1. Minimal + vectors.push( + await nonMalformed( + 'minimal-single-tlv', + base({}), + `Smallest valid invoice — all required fields, one item, no optional fields. ${WIRE_DIAG}`, + ), + ) + + // 2. Chain selectors (5) + const chains: Array<[number, string]> = [ + [1, 'ethereum'], + [8453, 'base'], + [42161, 'arbitrum'], + [10, 'optimism'], + [137, 'polygon'], + ] + for (const [network_id, chainName] of chains) { + vectors.push( + await nonMalformed( + `chain-${chainName}`, + base({ network_id, invoice_id: `INV-CHAIN-${network_id}` }), + `Chain selector: ${chainName} (network_id=${network_id}). ${WIRE_DIAG}`, + ), + ) + } + + // 3. BigInt edges (4) + + // 3a. amount = 0 + vectors.push( + await nonMalformed( + 'bigint-amount-zero', + base({ + invoice_id: 'INV-BIGINT-ZERO', + items: [{ description: 'Zero payment', quantity: 1.0, rate: '0' }], + total: '0', + }), + `BigInt edge: total = 0 (LEB128 single 0x00 byte). ${WIRE_DIAG}`, + ), + ) + + // 3b. amount = 1 + vectors.push( + await nonMalformed( + 'bigint-amount-one', + base({ + invoice_id: 'INV-BIGINT-ONE', + items: [{ description: 'One atomic unit', quantity: 1.0, rate: '1' }], + total: '1', + }), + `BigInt edge: total = 1 (smallest nonzero, no trailing zeros). ${WIRE_DIAG}`, + ), + ) + + // 3c. U256::MAX — largest value the U256 codec accepts without overflow. + // Codec widened to U256 in T-P2-12a: this must now encode successfully (roundtrip true). + const U256_MAX = '115792089237316195423570985008687907853269984665640564039457584007913129639935' + vectors.push( + await nonMalformed( + 'bigint-amount-uint256-max', + base({ + invoice_id: 'INV-BIGINT-U256MAX', + currency: 'ETH', + decimals: 18, + items: [{ description: 'Max uint256 payment', quantity: 1.0, rate: U256_MAX }], + total: U256_MAX, + }), + `BigInt edge: total = U256::MAX (${U256_MAX}) — largest encodable value after U256 widening. ${WIRE_DIAG}`, + ), + ) + + // 3d. 2^256 — one above U256::MAX, must produce InvalidAmount error. + // diagnostic: "malformed:encode-input" — error fires at encode time, no bytes produced. + // decoded field is present so T-P2-13 can construct the Invoice and assert InvalidAmount. + { + const OVER_U256 = '115792089237316195423570985008687907853269984665640564039457584007913129639936' + const overU256Invoice = base({ + invoice_id: 'INV-BIGINT-OVER-U256', + currency: 'ETH', + decimals: 18, + items: [{ description: 'Over U256 payment', quantity: 1.0, rate: OVER_U256 }], + total: OVER_U256, + }) + try { + encodeInvoiceCanonical(overU256Invoice) + throw new Error('Expected InvalidAmount error but encode succeeded — codec regression') + } catch (err: unknown) { + if (err instanceof Error && err.message.startsWith('Expected InvalidAmount')) throw err + // encode threw as expected — no bytes produced + } + vectors.push({ + name: 'bigint-amount-over-u256', + decoded: overU256Invoice, + diagnostic: 'malformed:encode-input', + expected_error: 'InvalidAmount', + } as MalformedVector) + } + + // 3e. malformed-checksum-mismatch — bytes with valid header + COUNT=1 but payload + // has no valid domain-separator/checksum TLV → ChecksumMismatch. + // This is the corrected classification of the original "malformed-varint-overflow" + // vector (hex is unchanged; only name + expected_error corrected per Kai decision + // 2026-05-20: the codec hits ChecksumMismatch before any varint overflow path). + { + const checksumBytes = new Uint8Array( + Buffer.from( + '56010118268080808080808080808080808080808080808080808080808080808080808080808080808080', + 'hex', + ), + ) + vectors.push({ + name: 'malformed-checksum-mismatch', + canonical_hex: toHex(checksumBytes), + diagnostic: 'malformed:canonical', + expected_error: 'ChecksumMismatch', + }) + } + + // 3f. malformed-varint-overflow — crafted bytes where the LENGTH field of the + // first TLV record is a varint with 37 continuation bytes and no terminator. + // Wire: MAGIC VERSION COUNT=1 TYPE=0x18 [37× 0x80 with MSB set, no terminal byte] + // read_varint fires VarintOverflow at bytes_read == MAX_BYTES (37) before reaching + // the checksum validation stage. + { + const buf = new Uint8Array(4 + 37) + buf[0] = 0x56 // MAGIC + buf[1] = 0x01 // VERSION + buf[2] = 0x01 // COUNT=1 + buf[3] = 0x18 // TLV type=24 (TLV_TOTAL) — type byte is valid; overflow is in LENGTH + buf.fill(0x80, 4) // 37 bytes all with continuation bit set, no terminal → VarintOverflow + vectors.push({ + name: 'malformed-varint-overflow', + canonical_hex: toHex(buf), + diagnostic: 'malformed:canonical', + expected_error: 'VarintOverflow', + }) + } + + // 4. Extensions (3) + + // 4a. magic-dust: micro-amount uniquifier in total + vectors.push( + await nonMalformed( + 'extension-magic-dust', + base({ + invoice_id: 'INV-EXT-DUST', + total: '1000042', + notes: 'Magic dust applied: +0.000042 for unique matching', + items: [{ description: 'Consulting', quantity: 1.0, rate: '1000042' }], + }), + `Extension: magic-dust (micro-amount uniquifier in total + notes field). ${WIRE_DIAG}`, + ), + ) + + // 4b. OG-param: from.email + client.wallet_address + notes + vectors.push( + await nonMalformed( + 'extension-og-param', + base({ + invoice_id: 'INV-EXT-OG', + from: { name: 'Alice Dev Studio', wallet_address: FROM_WALLET, email: 'alice@dev.io' }, + client: { name: 'Acme Corp', wallet_address: CLIENT_WALLET }, + notes: 'Please pay within 30 days', + total: '5000000', + items: [{ description: 'Design work', quantity: 1.0, rate: '5000000' }], + }), + `Extension: OG-param fields (from.email, client.wallet_address, notes) for social preview. ${WIRE_DIAG}`, + ), + ) + + // 4c. sub-invoice-chain: ETH on Arbitrum with tax + discount + vectors.push( + await nonMalformed( + 'extension-sub-invoice-chain', + base({ + invoice_id: 'INV-EXT-SUBCHAIN', + network_id: 42161, + currency: 'ETH', + decimals: 18, + total: '500000000000000000', + items: [{ description: 'Cross-chain consulting', quantity: 1.0, rate: '500000000000000000' }], + tax: '10', + discount: '5', + }), + `Extension: sub-invoice chain — ETH on Arbitrum with tax and discount fields. ${WIRE_DIAG}`, + ), + ) + + // 5. Malformed (3) + + // 5a. Corrupted brotli: COMPRESSED_FLAG set, body is not valid Brotli + { + const bytes = new Uint8Array([0x56, 0x81, 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]) + vectors.push({ + name: 'malformed-corrupted-brotli', + wire_hex: toHex(bytes), + diagnostic: 'malformed:wire', + expected_error: 'CompressionFailed', + }) + } + + // 5b. Oversize: claims a 1494-byte TLV value but the buffer has only 4 bytes → Truncated + { + const bytes = new Uint8Array(10) + bytes[0] = 0x56; bytes[1] = 0x01; bytes[2] = 0x01 + bytes[3] = 0x18 // TLV_TOTAL=24 + bytes[4] = 0xd6; bytes[5] = 0x0b // LEB128(1494) + // bytes[6..9] = 0x00 — far fewer than claimed 1494 + vectors.push({ + name: 'malformed-oversize', + canonical_hex: toHex(bytes), + diagnostic: 'malformed:canonical', + expected_error: 'Truncated', + }) + } + + // 5c. Bad magic: first byte is not 0x56 + { + const bytes = new Uint8Array([0xff, 0x01, 0x01, 0x18, 0x02, 0x01, 0x00]) + vectors.push({ + name: 'malformed-bad-magic', + canonical_hex: toHex(bytes), + diagnostic: 'malformed:canonical', + expected_error: 'BadMagic', + }) + } + + // --------------------------------------------------------------------------- + // Write output + // --------------------------------------------------------------------------- + + const output = { + schema_version: 1, + generated_by: '@void-layer/codec v0.0.0', + generated_at: '2026-05-20', + vectors, + } + + fs.mkdirSync(VECTORS_DIR, { recursive: true }) + fs.writeFileSync(OUT_PATH, JSON.stringify(output, null, 2) + '\n') + + console.log(`\nGenerated ${vectors.length} vectors → ${OUT_PATH}\n`) + for (const v of vectors) { + if ('expected_error' in v) { + const mv = v as MalformedVector + const hex = mv.canonical_hex ?? mv.wire_hex ?? '' + console.log( + ` [MALFORMED] ${mv.name.padEnd(38)} hex_len=${String(hex.length).padStart(4)} expected_error=${mv.expected_error}`, + ) + } else { + const nv = v as NonMalformedVector + const comp = isCompressed(nv.wire_hex) + console.log( + ` [OK] ${nv.name.padEnd(38)} canonical_hex_len=${String(nv.canonical_hex.length).padStart(4)} wire_compressed=${comp} roundtrip=${nv.roundtrip}`, + ) + } + } + + const failed = vectors.filter( + (v) => !('expected_error' in v) && !(v as NonMalformedVector).roundtrip, + ) + if (failed.length > 0) { + console.error(`\nROUNDTRIP FAILURES: ${failed.map((v) => v.name).join(', ')}`) + process.exit(1) + } + console.log('\nAll roundtrips: OK') +} + +main().catch((err) => { + console.error('Vector generation failed:', err) + process.exit(1) +}) diff --git a/packages/codec/scripts/run-generate-vectors.test.ts b/packages/codec/scripts/run-generate-vectors.test.ts new file mode 100644 index 0000000..3f8f2f3 --- /dev/null +++ b/packages/codec/scripts/run-generate-vectors.test.ts @@ -0,0 +1,16 @@ +/** + * Vitest wrapper — runs the golden vector generator as a test so vitest's + * module resolver (brotli-wasm alias, wasm plugin) are active. + * + * Usage: pnpm -C packages/codec exec vitest run scripts/run-generate-vectors.test.ts + */ +import { test } from 'vitest' + +test('generate golden vectors', async () => { + // Dynamic import picks up vitest's alias resolution for brotli-wasm + const mod = await import('./generate-vectors.js') + // The module calls main() at module level via the bottom invocation. + // If we import it directly it runs. But generate-vectors.ts exports nothing + // and has a top-level main() call — it already ran on import. + void mod +}, 120_000) diff --git a/packages/codec/src/decode.rs b/packages/codec/src/decode.rs new file mode 100644 index 0000000..b1e1ee5 --- /dev/null +++ b/packages/codec/src/decode.rs @@ -0,0 +1,796 @@ +// Mirrors vl/app/src/features/invoice-codec/lib/decode.ts +// and vl/app/src/shared/lib/tlv-codec/{reader.ts,varint.ts}. +// +// Reads: [MAGIC][VERSION][COUNT][TLV records...] +// Validates: magic, version (no COMPRESSED_FLAG), canonical ordering, domain separator. +// Maps TLV types to Invoice fields per tlv-map.ts. + +use std::collections::BTreeMap; + +use crate::dict::chain::CHAIN_DICT; +use crate::encode::{ + COMPRESSED_FLAG, MAGIC, TLV_CHAIN_ID, TLV_CLIENT_ADDRESS, TLV_CLIENT_EMAIL, TLV_CLIENT_NAME, + TLV_CLIENT_PHONE, TLV_CLIENT_TAX_ID, TLV_CLIENT_WALLET, TLV_CURRENCY, TLV_DECIMALS, + TLV_DISCOUNT, TLV_DOMAIN_SEPARATOR, TLV_DUE_AT, TLV_FROM_ADDRESS, TLV_FROM_EMAIL, + TLV_FROM_NAME, TLV_FROM_PHONE, TLV_FROM_TAX_ID, TLV_FROM_WALLET, TLV_INVOICE_ID, TLV_ISSUED_AT, + TLV_ITEMS, TLV_NOTES, TLV_SALT, TLV_TAX, TLV_TOKEN_ADDRESS, TLV_TOTAL, VERSION, +}; +use crate::error::CodecError; +use crate::hash::keccak256; +use crate::invoice::{Invoice, InvoiceClient, InvoiceFrom, InvoiceItem}; +use crate::tlv::read_tlv_stream; +use crate::varint::{read_bigint_varint, read_varint}; + +const MAX_TLV_COUNT: usize = 64; +const MAX_ITEMS: usize = 50; +const MAX_VALUE_SIZE: usize = 4096; + +// --------------------------------------------------------------------------- +// Private decode helpers +// --------------------------------------------------------------------------- + +/// Decode 20 raw bytes to a 0x-prefixed lowercase hex address. +fn bytes_to_address(bytes: &[u8]) -> Result { + if bytes.len() != 20 { + return Err(CodecError::Truncated { + needed: 20, + had: bytes.len(), + }); + } + use std::fmt::Write as _; + let mut hex = String::with_capacity(42); + hex.push_str("0x"); + for b in bytes { + let _ = write!(hex, "{b:02x}"); + } + Ok(hex) +} + +/// Decode raw bytes to a lowercase hex string (for salt, arbitrary length). +fn bytes_to_hex(bytes: &[u8]) -> String { + use std::fmt::Write as _; + let mut hex = String::with_capacity(bytes.len() * 2); + for b in bytes { + let _ = write!(hex, "{b:02x}"); + } + hex +} + +/// Reverse app-level dictionary substitution (mirrors reverseDict from app-dict.ts). +fn reverse_dict(bytes: &[u8]) -> Result { + // Decode raw bytes as a string — control chars are the dict codes + let mut text = String::with_capacity(bytes.len()); + for &b in bytes { + text.push(b as char); + } + + // Reverse entries longest-pattern-first (same order as apply_dict) + let entries: &[(&str, u8)] = &[ + ("@outlook.com", 0x02), + ("@hotmail.com", 0x0c), + ("development", 0x0d), + ("consulting", 0x0e), + ("@gmail.com", 0x03), + ("@yahoo.com", 0x04), + ("https://", 0x05), + ("Invoice", 0x06), + ("Payment", 0x07), + (".com", 0x09), + ("INV-", 0x0f), + ]; + + // Apply in reverse order (shortest first for reverse) — mirrors TS [...DICT_ENTRIES].reverse() + for &(pattern, code) in entries.iter().rev() { + text = text.replace(char::from(code), pattern); + } + + Ok(text) +} + +/// Decode chain ID from TLV value bytes: +/// [0x00, code] → dict lookup +/// [0x01, varint...] → raw chain ID +fn decode_chain_id(value: &[u8]) -> Result { + if value.is_empty() { + return Err(CodecError::Truncated { needed: 2, had: 0 }); + } + let prefix = value[0]; + if prefix == 0x00 { + if value.len() < 2 { + return Err(CodecError::Truncated { needed: 2, had: 1 }); + } + let code = value[1]; + // Reverse lookup: code → chain_id + let chain_id = CHAIN_DICT + .entries() + .find(|&(&_k, &v)| v == code) + .map(|(&k, _)| k) + .ok_or(CodecError::UnknownExtension(code))?; + Ok(chain_id) + } else if prefix == 0x01 { + let (chain_id, _) = read_varint(value, 1)?; + Ok(chain_id as u32) + } else { + Err(CodecError::UnknownExtension(prefix)) + } +} + +/// Currency code → symbol (mirrors CURRENCY_DICT_REVERSE in tlv-map.ts). Static: zero per-call alloc. +static CURRENCY_CODE_TO_SYMBOL: &[(u8, &str)] = &[ + (1, "USDC"), + (2, "USDT"), + (3, "DAI"), + (4, "ETH"), + (5, "WETH"), + (6, "MATIC"), + (7, "POL"), + (8, "WBTC"), + (9, "USDC.E"), + (10, "EURC"), + (11, "USDT0"), +]; + +/// Token dict code → lowercase address (mirrors TOKEN_DICT_REVERSE in tlv-map.ts). Static: zero per-call alloc. +/// Code 43 = Base WETH (same address as Optimism code 24, different chain context). +static TOKEN_CODE_TO_ADDRESS: &[(u8, &str)] = &[ + (1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + (2, "0xdac17f958d2ee523a2206206994597c13d831ec7"), + (3, "0x6b175474e89094c44da98b954eedeac495271d0f"), + (4, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + (5, "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599"), + (6, "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c"), + (7, "0x6c96de32cea08842dcc4058c14d3aaad7fa41dee"), + (10, "0xaf88d065e77c8cc2239327c5edb3a432268e5831"), + (11, "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"), + (12, "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"), + (13, "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"), + (14, "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"), + (15, "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"), + (20, "0x0b2c639c533813f4aa9d7837caf62653d097ff85"), + (21, "0x7f5c764cbc14f9669b88837ca1490cca17c31607"), + (22, "0x94b008aa00579c1307b0ef2c499ad98a8ce58e58"), + (24, "0x4200000000000000000000000000000000000006"), + (25, "0x68f180fcce6836688e9084f035309e29bf0a2095"), + (30, "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"), + (31, "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"), + (32, "0xc2132d05d31c914a87c6611c10748aeb04b58e8f"), + (33, "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063"), + (34, "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619"), + (35, "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6"), + (40, "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), + (41, "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca"), + (42, "0x50c5725949a6f0c72e6c4a641f24049a917db0cb"), + (43, "0x4200000000000000000000000000000000000006"), + (44, "0x0555e30da8f98308edb960aa94c0ed47230d2b9c"), + (45, "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42"), +]; + +/// Decode currency from TLV value bytes: +/// [0x00, code] → dict lookup +/// [0x01, utf8...] → raw string +fn decode_currency(value: &[u8]) -> Result { + if value.is_empty() { + return Err(CodecError::Truncated { needed: 2, had: 0 }); + } + if value[0] == 0x00 { + if value.len() < 2 { + return Err(CodecError::Truncated { needed: 2, had: 1 }); + } + let code = value[1]; + CURRENCY_CODE_TO_SYMBOL + .iter() + .find(|&&(c, _)| c == code) + .map(|&(_, s)| s.to_string()) + .ok_or(CodecError::UnknownExtension(code)) + } else { + String::from_utf8(value[1..].to_vec()) + .map_err(|_| CodecError::CompressionFailed("invalid UTF-8 in currency".to_string())) + } +} + +/// Decode token address from TLV value bytes: +/// [0x00, code] → dict reverse lookup +/// [0x01, 20 bytes] → raw hex address +fn decode_token_address(value: &[u8]) -> Result { + if value.is_empty() { + return Err(CodecError::Truncated { needed: 2, had: 0 }); + } + if value[0] == 0x00 { + if value.len() < 2 { + return Err(CodecError::Truncated { needed: 2, had: 1 }); + } + let code = value[1]; + TOKEN_CODE_TO_ADDRESS + .iter() + .find(|&&(c, _)| c == code) + .map(|&(_, addr)| addr.to_string()) + .ok_or(CodecError::UnknownExtension(code)) + } else { + bytes_to_address(&value[1..]) + } +} + +/// Decode mantissa-encoded amount from bytes (mirrors readMantissa from varint.ts). +/// Returns amount as a decimal string (BigInt-safe). +fn decode_mantissa(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(CodecError::Truncated { needed: 2, had: 0 }); + } + let (mantissa_bytes, m_consumed) = read_bigint_varint(bytes, 0)?; + let zeros_offset = m_consumed; + if zeros_offset >= bytes.len() { + return Err(CodecError::Truncated { + needed: zeros_offset + 1, + had: bytes.len(), + }); + } + let zeros = bytes[zeros_offset] as u32; + if zeros > 30 { + return Err(CodecError::CompressionFailed(format!( + "mantissa trailing zeros {zeros} exceeds maximum 30" + ))); + } + + // Reconstruct value: mantissa_bytes is big-endian → U256 + use ruint::aliases::U256; + if mantissa_bytes.len() > 32 { + return Err(CodecError::InvalidAmount(format!( + "mantissa varint too large: {} bytes exceeds U256", + mantissa_bytes.len() + ))); + } + let mut be32 = [0u8; 32]; + be32[32 - mantissa_bytes.len()..].copy_from_slice(&mantissa_bytes); + let mantissa = U256::from_be_bytes(be32); + let scale = U256::from(10u64).pow(U256::from(zeros)); + let value = mantissa + .checked_mul(scale) + .ok_or_else(|| CodecError::InvalidAmount("amount overflow U256".to_string()))?; + Ok(value.to_string()) +} + +/// Decode packed items from Type 14 binary format (mirrors unpackItems from decode.ts). +fn unpack_items(data: &[u8]) -> Result, CodecError> { + let mut offset = 0; + let (count, n) = read_varint(data, offset)?; + offset += n; + let count = count as usize; + if count > MAX_ITEMS { + return Err(CodecError::CompressionFailed(format!( + "item count {count} exceeds max {MAX_ITEMS}" + ))); + } + + let mut items = Vec::with_capacity(count); + for i in 0..count { + // description length + if offset >= data.len() { + return Err(CodecError::Truncated { + needed: offset + 1, + had: data.len(), + }); + } + let (desc_len, n) = read_varint(data, offset)?; + offset += n; + let desc_len = desc_len as usize; + if offset + desc_len > data.len() { + return Err(CodecError::Truncated { + needed: offset + desc_len, + had: data.len(), + }); + } + let desc_bytes = &data[offset..offset + desc_len]; + let description = reverse_dict(desc_bytes)?; + offset += desc_len; + + // quantity: [scale: u8][scaled_value: varint] + if offset >= data.len() { + return Err(CodecError::Truncated { + needed: offset + 1, + had: data.len(), + }); + } + let scale = data[offset] as u32; + offset += 1; + let (scaled_value, n) = read_varint(data, offset)?; + offset += n; + let quantity = scaled_value as f64 / 10f64.powi(scale as i32); + + // rate: mantissa + trailing zeros + let (mantissa_be, m_n) = read_bigint_varint(data, offset)?; + offset += m_n; + if offset >= data.len() { + return Err(CodecError::Truncated { + needed: offset + 1, + had: data.len(), + }); + } + let zeros = data[offset] as u32; + offset += 1; + if zeros > 30 { + return Err(CodecError::CompressionFailed(format!( + "item {i} rate zeros {zeros} exceeds max 30" + ))); + } + + use ruint::aliases::U256; + if mantissa_be.len() > 32 { + return Err(CodecError::InvalidAmount(format!( + "item {i} rate mantissa varint too large: {} bytes exceeds U256", + mantissa_be.len() + ))); + } + let mut be32 = [0u8; 32]; + be32[32 - mantissa_be.len()..].copy_from_slice(&mantissa_be); + let mantissa = U256::from_be_bytes(be32); + let scale = U256::from(10u64).pow(U256::from(zeros)); + let rate = mantissa + .checked_mul(scale) + .ok_or_else(|| CodecError::InvalidAmount(format!("item {i} rate overflow U256")))? + .to_string(); + + items.push(InvoiceItem { + description, + quantity, + rate, + }); + } + Ok(items) +} + +/// Verify domain separator (mirrors validateSecurity from security.ts). +fn verify_domain_separator( + records: &BTreeMap>, + stored_sep: &[u8], +) -> Result<(), CodecError> { + let prefix = b"VOIDPAY_INVOICE_V1"; + let mut body: Vec = prefix.to_vec(); + + for (&tlv_type, value) in records { + if tlv_type == TLV_DOMAIN_SEPARATOR { + continue; + } + body.push(tlv_type); + crate::varint::write_varint(value.len() as u64, &mut body); + body.extend_from_slice(value); + } + + let expected = keccak256(&body); + if expected != stored_sep { + return Err(CodecError::ChecksumMismatch); + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// Decode canonical pre-compression bytes into an [`Invoice`]. +/// +/// Accepts the raw TLV binary output of [`encode_invoice_canonical`]. +/// Rejects payloads with the COMPRESSED_FLAG set — those must be decompressed +/// by the JS shim before being passed here. +/// +/// # Errors +/// - [`CodecError::BadMagic`] — wrong magic byte or empty input +/// - [`CodecError::UnsupportedVersion`] — version byte is not 0x01 +/// - [`CodecError::Truncated`] — payload too short +/// - [`CodecError::ChecksumMismatch`] — domain separator mismatch +/// +/// # Example +/// ``` +/// use void_layer_codec::{encode_invoice_canonical, decode_invoice_canonical}; +/// use void_layer_codec::{Invoice, InvoiceFrom, InvoiceClient, InvoiceItem}; +/// let invoice = Invoice { +/// invoice_id: "INV-001".to_string(), +/// issued_at: 1_700_000_000, due_at: 1_700_604_800, +/// network_id: 1, currency: "USDC".to_string(), decimals: 6, +/// from: InvoiceFrom { +/// name: "Alice".to_string(), +/// wallet_address: "0xaabbccddee0011223344556677889900aabbccdd".to_string(), +/// email: None, phone: None, physical_address: None, tax_id: None, +/// }, +/// client: InvoiceClient { +/// name: "Bob".to_string(), wallet_address: None, +/// email: None, phone: None, physical_address: None, tax_id: None, +/// }, +/// items: vec![InvoiceItem { +/// description: "Work".to_string(), quantity: 1.0, rate: "1000000".to_string(), +/// }], +/// token_address: None, notes: None, tax: None, discount: None, +/// total: "1000000".to_string(), +/// salt: "00112233445566778899aabbccddeeff".to_string(), +/// }; +/// let bytes = encode_invoice_canonical(&invoice).unwrap(); +/// let decoded = decode_invoice_canonical(&bytes).unwrap(); +/// assert_eq!(decoded.invoice_id, "INV-001"); +/// ``` +pub fn decode_invoice_canonical(bytes: &[u8]) -> Result { + if bytes.is_empty() || bytes[0] != MAGIC { + return Err(CodecError::BadMagic); + } + + if bytes.len() < 2 { + return Err(CodecError::Truncated { needed: 3, had: 1 }); + } + + let version_byte = bytes[1]; + // Reject compressed payloads — COMPRESSED_FLAG (0x80) means JS shim must Brotli-decompress first. + // decode_invoice_canonical is the identity-boundary function; it only accepts raw canonical bytes. + if version_byte & COMPRESSED_FLAG != 0 { + return Err(CodecError::CompressionFailed( + "unexpected compressed input in decode_invoice_canonical — decompress first" + .to_string(), + )); + } + if version_byte != VERSION { + return Err(CodecError::UnsupportedVersion(version_byte)); + } + + if bytes.len() < 3 { + return Err(CodecError::Truncated { needed: 3, had: 2 }); + } + + let tlv_count = bytes[2] as usize; + if tlv_count > MAX_TLV_COUNT { + return Err(CodecError::CompressionFailed(format!( + "TLV count {tlv_count} exceeds max {MAX_TLV_COUNT}" + ))); + } + + let tlv_body = &bytes[3..]; + let records: BTreeMap> = read_tlv_stream(tlv_body)?; + + if records.len() != tlv_count { + return Err(CodecError::Truncated { + needed: tlv_count, + had: records.len(), + }); + } + + for (&tlv_type, value) in &records { + if value.len() > MAX_VALUE_SIZE { + return Err(CodecError::CompressionFailed(format!( + "TLV type {tlv_type} value size {} exceeds max {MAX_VALUE_SIZE}", + value.len() + ))); + } + } + + let salt_bytes = records.get(&TLV_SALT).ok_or(CodecError::ChecksumMismatch)?; + if salt_bytes.len() < 16 { + return Err(CodecError::ChecksumMismatch); + } + + let stored_sep = records + .get(&TLV_DOMAIN_SEPARATOR) + .ok_or(CodecError::ChecksumMismatch)?; + verify_domain_separator(&records, stored_sep)?; + + let chain_id_bytes = records.get(&TLV_CHAIN_ID).ok_or(CodecError::BadMagic)?; + let network_id = decode_chain_id(chain_id_bytes)?; + + let issued_at_bytes = records + .get(&TLV_ISSUED_AT) + .ok_or(CodecError::Truncated { needed: 4, had: 0 })?; + if issued_at_bytes.len() < 4 { + return Err(CodecError::Truncated { + needed: 4, + had: issued_at_bytes.len(), + }); + } + let issued_at = u32::from_be_bytes([ + issued_at_bytes[0], + issued_at_bytes[1], + issued_at_bytes[2], + issued_at_bytes[3], + ]); + + let due_at_bytes = records + .get(&TLV_DUE_AT) + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + let (due_delta, _) = read_varint(due_at_bytes, 0)?; + let due_at = issued_at + due_delta as u32; + + let decimals_bytes = records + .get(&TLV_DECIMALS) + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + let decimals = *decimals_bytes + .first() + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + + let from_wallet_bytes = records + .get(&TLV_FROM_WALLET) + .ok_or(CodecError::Truncated { needed: 20, had: 0 })?; + let from_wallet_address = bytes_to_address(from_wallet_bytes)?; + + let currency_bytes = records + .get(&TLV_CURRENCY) + .ok_or(CodecError::Truncated { needed: 2, had: 0 })?; + let currency = decode_currency(currency_bytes)?; + + let items_bytes = records + .get(&TLV_ITEMS) + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + let items = unpack_items(items_bytes)?; + + let from_name_bytes = records + .get(&TLV_FROM_NAME) + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + let from_name = reverse_dict(from_name_bytes)?; + + let client_name_bytes = records + .get(&TLV_CLIENT_NAME) + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + let client_name = reverse_dict(client_name_bytes)?; + + let invoice_id_bytes = records + .get(&TLV_INVOICE_ID) + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; + let invoice_id = String::from_utf8(invoice_id_bytes.clone()) + .map_err(|_| CodecError::CompressionFailed("invalid UTF-8 in invoice_id".to_string()))?; + + let total_bytes = records + .get(&TLV_TOTAL) + .ok_or(CodecError::Truncated { needed: 2, had: 0 })?; + let total = decode_mantissa(total_bytes)?; + + let salt_hex = bytes_to_hex(salt_bytes); + + let token_address = if let Some(v) = records.get(&TLV_TOKEN_ADDRESS) { + Some(decode_token_address(v)?) + } else { + None + }; + + let client_wallet_address = if let Some(v) = records.get(&TLV_CLIENT_WALLET) { + Some(bytes_to_address(v)?) + } else { + None + }; + + let notes = if let Some(v) = records.get(&TLV_NOTES) { + Some(reverse_dict(v)?) + } else { + None + }; + + let from_email = if let Some(v) = records.get(&TLV_FROM_EMAIL) { + Some(reverse_dict(v)?) + } else { + None + }; + + let from_phone = if let Some(v) = records.get(&TLV_FROM_PHONE) { + Some(reverse_dict(v)?) + } else { + None + }; + + let from_physical_address = if let Some(v) = records.get(&TLV_FROM_ADDRESS) { + Some(reverse_dict(v)?) + } else { + None + }; + + let from_tax_id = if let Some(v) = records.get(&TLV_FROM_TAX_ID) { + Some(reverse_dict(v)?) + } else { + None + }; + + let client_email = if let Some(v) = records.get(&TLV_CLIENT_EMAIL) { + Some(reverse_dict(v)?) + } else { + None + }; + + let client_phone = if let Some(v) = records.get(&TLV_CLIENT_PHONE) { + Some(reverse_dict(v)?) + } else { + None + }; + + let client_physical_address = if let Some(v) = records.get(&TLV_CLIENT_ADDRESS) { + Some(reverse_dict(v)?) + } else { + None + }; + + let client_tax_id = if let Some(v) = records.get(&TLV_CLIENT_TAX_ID) { + Some(reverse_dict(v)?) + } else { + None + }; + + let tax = if let Some(v) = records.get(&TLV_TAX) { + Some( + String::from_utf8(v.clone()) + .map_err(|_| CodecError::CompressionFailed("invalid UTF-8 in tax".to_string()))?, + ) + } else { + None + }; + + let discount = + if let Some(v) = records.get(&TLV_DISCOUNT) { + Some(String::from_utf8(v.clone()).map_err(|_| { + CodecError::CompressionFailed("invalid UTF-8 in discount".to_string()) + })?) + } else { + None + }; + + Ok(Invoice { + invoice_id, + issued_at, + due_at, + network_id, + currency, + decimals, + from: InvoiceFrom { + name: from_name, + wallet_address: from_wallet_address, + email: from_email, + phone: from_phone, + physical_address: from_physical_address, + tax_id: from_tax_id, + }, + client: InvoiceClient { + name: client_name, + wallet_address: client_wallet_address, + email: client_email, + phone: client_phone, + physical_address: client_physical_address, + tax_id: client_tax_id, + }, + items, + token_address, + notes, + tax, + discount, + total, + salt: salt_hex, + }) +} + +// --------------------------------------------------------------------------- +// Test helpers (pub only under #[cfg(test)]) +// --------------------------------------------------------------------------- + +#[cfg(test)] +pub(crate) mod tests_pub { + use super::*; + + pub(crate) fn decode_mantissa_pub(bytes: &[u8]) -> Result { + decode_mantissa(bytes) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_mantissa_zero() { + // encode: mantissa=0 → [0x00, 0x00] + let result = decode_mantissa(&[0x00, 0x00]).unwrap(); + assert_eq!(result, "0"); + } + + #[test] + fn decode_mantissa_one_million() { + // mantissa=1 (0x01), zeros=6 → 1_000_000 + let result = decode_mantissa(&[0x01, 0x06]).unwrap(); + assert_eq!(result, "1000000"); + } + + #[test] + fn decode_mantissa_123() { + // mantissa=123 (0x7B), zeros=0 + let result = decode_mantissa(&[0x7b, 0x00]).unwrap(); + assert_eq!(result, "123"); + } + + #[test] + fn decode_chain_id_known_ethereum() { + let result = decode_chain_id(&[0x00, 0x01]).unwrap(); + assert_eq!(result, 1); + } + + #[test] + fn decode_chain_id_known_base() { + let result = decode_chain_id(&[0x00, 0x05]).unwrap(); + assert_eq!(result, 8453); + } + + #[test] + fn decode_currency_known_usdc() { + let result = decode_currency(&[0x00, 0x01]).unwrap(); + assert_eq!(result, "USDC"); + } + + #[test] + fn decode_currency_raw() { + let mut v = vec![0x01u8]; + v.extend_from_slice(b"XYZ"); + let result = decode_currency(&v).unwrap(); + assert_eq!(result, "XYZ"); + } + + #[test] + fn bytes_to_address_roundtrip() { + let addr = "0xaabbccddee0011223344556677889900aabbccdd"; + let raw: Vec = (0..20) + .map(|i| u8::from_str_radix(&addr[2 + i * 2..4 + i * 2], 16).unwrap()) + .collect(); + let result = bytes_to_address(&raw).unwrap(); + assert_eq!(result, addr); + } + + #[test] + fn reverse_dict_invoice() { + // 0x06 is dict code for "Invoice" + let result = reverse_dict(&[0x06]).unwrap(); + assert_eq!(result, "Invoice"); + } + + #[test] + fn reverse_dict_passthrough() { + let result = reverse_dict(b"Hello world").unwrap(); + assert_eq!(result, "Hello world"); + } + + // --- U256 mantissa decode tests --- + + #[test] + fn decode_mantissa_u256_max_roundtrip() { + // Encode u256::MAX via encode path then decode — end-to-end parity check. + use crate::encode::tests_pub::mantissa_bytes_pub; + let uint256_max = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; + let encoded = mantissa_bytes_pub(uint256_max).unwrap(); + let decoded = decode_mantissa(&encoded).unwrap(); + assert_eq!(decoded, uint256_max); + } + + #[test] + fn decode_mantissa_large_value_above_u128() { + // A value between u128::MAX and u256::MAX — old code would silently saturate. + use crate::encode::tests_pub::mantissa_bytes_pub; + // u128::MAX * 1000 (well above u128 range) + let large = "340282366920938463463374607431768211455000"; + let encoded = mantissa_bytes_pub(large).unwrap(); + let decoded = decode_mantissa(&encoded).unwrap(); + assert_eq!(decoded, large); + } + + #[test] + fn decode_mantissa_wire_payload_exceeding_u256_errors() { + // Craft a wire payload whose mantissa varint decodes to 33 bytes (> 32) — must error + // cleanly, never silently saturate (the old u128 saturation bug). + // A 33-byte all-0xFF big-endian value encoded as LEB128 exceeds MAX_BYTES (37 × 7-bit + // chunks = 259 bits > 256 bits) so the varint layer returns VarintOverflow before the + // 32-byte U256 guard fires. Both VarintOverflow and InvalidAmount are CodecError + // variants — either satisfies the "no silent saturation" requirement. + use crate::varint::write_bigint_varint; + let oversized_mantissa = vec![0xFFu8; 33]; // 33 bytes > U256 max 32 bytes + let mut payload = Vec::new(); + write_bigint_varint(&oversized_mantissa, &mut payload); + payload.push(0u8); // zeros = 0 + + let err = decode_mantissa(&payload).unwrap_err(); + assert!( + matches!( + err, + CodecError::InvalidAmount(_) | CodecError::VarintOverflow(_) + ), + "expected InvalidAmount or VarintOverflow for oversized mantissa, got {err:?}" + ); + } +} diff --git a/packages/codec/src/dict/app.rs b/packages/codec/src/dict/app.rs new file mode 100644 index 0000000..87ef08e --- /dev/null +++ b/packages/codec/src/dict/app.rs @@ -0,0 +1,24 @@ +// Dead-code lint suppressed: pub(crate) statics consumed by encode/decode in Phase 2B; +// #[expect] incompatible with inline-test target (lint never fires on test binary → unfulfilled_lint_expectations). +#![allow(dead_code)] + +use phf::phf_map; + +/// Application-level text dictionary — pre-Brotli substitution for common patterns. +/// +/// Maps string pattern → 1-byte control code (0x02–0x1F range). +/// Entries are in length-descending order (longest match first) to avoid partial replacements. +/// This map is append-only forever (Constitution IV). +pub(crate) static APP_DICT: phf::Map<&'static str, u8> = phf_map! { + "@outlook.com" => 0x02u8, + "@hotmail.com" => 0x0cu8, + "development" => 0x0du8, + "consulting" => 0x0eu8, + "@gmail.com" => 0x03u8, + "@yahoo.com" => 0x04u8, + "https://" => 0x05u8, + "Invoice" => 0x06u8, + "Payment" => 0x07u8, + ".com" => 0x09u8, + "INV-" => 0x0fu8, +}; diff --git a/packages/codec/src/dict/chain.rs b/packages/codec/src/dict/chain.rs new file mode 100644 index 0000000..49298c5 --- /dev/null +++ b/packages/codec/src/dict/chain.rs @@ -0,0 +1,20 @@ +// Dead-code lint suppressed: pub(crate) statics consumed by encode/decode in Phase 2B; +// #[expect] incompatible with inline-test target (lint never fires on test binary → unfulfilled_lint_expectations). +#![allow(dead_code)] + +use phf::phf_map; + +/// Chain ID dictionary — maps known EVM chain IDs to 1-byte dict codes. +/// +/// Encoding scheme (mirror of TS chain-dict.ts): +/// 0x00 — known chain (dict lookup, 2 bytes total) +/// 0x01 — unknown chain (raw varint, 2+ bytes total) +/// +/// This map is append-only forever (Constitution IV). +pub(crate) static CHAIN_DICT: phf::Map = phf_map! { + 1u32 => 0x01u8, // Ethereum + 42161u32 => 0x02u8, // Arbitrum + 10u32 => 0x03u8, // Optimism + 137u32 => 0x04u8, // Polygon + 8453u32 => 0x05u8, // Base +}; diff --git a/packages/codec/src/dict/mod.rs b/packages/codec/src/dict/mod.rs new file mode 100644 index 0000000..9ff889f --- /dev/null +++ b/packages/codec/src/dict/mod.rs @@ -0,0 +1,130 @@ +// Dead-code lint suppressed: pub(crate) dict statics consumed by encode/decode in Phase 2B; +// #[expect] incompatible with inline-test target (lint never fires on test binary → unfulfilled_lint_expectations). +#![allow(dead_code)] + +pub(crate) mod app; +pub(crate) mod chain; + +#[cfg(test)] +mod tests { + use super::app::APP_DICT; + use super::chain::CHAIN_DICT; + use std::fmt::Write as _; + use tiny_keccak::{Hasher, Keccak}; + + // Two-commit pattern: run tests once with , capture actual hashes from + // failure output, then paste them here and commit again. + const APP_DICT_HASH: &str = "8abb746c2f968c2bde2b450aee01ce88aabe9df4bb8938bd6d02b587b4954b2e"; + const CHAIN_DICT_HASH: &str = + "6ddf0a04233a8b0b6dffe4658782eb5bd13391b37d202894e4da66efc5b388da"; + + fn to_hex(bytes: &[u8]) -> String { + bytes.iter().fold(String::new(), |mut acc, b| { + let _ = write!(acc, "{b:02x}"); + acc + }) + } + + fn keccak256_hex(data: &[u8]) -> String { + let mut k = Keccak::v256(); + let mut out = [0u8; 32]; + k.update(data); + k.finalize(&mut out); + to_hex(&out) + } + + /// Hash all APP_DICT entries: for each entry iterate sorted keys, + /// feed (key_bytes || value_byte) into keccak256. + fn hash_app_dict() -> String { + let mut keys: Vec<&'static str> = APP_DICT.keys().copied().collect(); + keys.sort_unstable(); + let mut k = Keccak::v256(); + for key in &keys { + k.update(key.as_bytes()); + k.update(&[*APP_DICT.get(key).unwrap()]); + } + let mut out = [0u8; 32]; + k.finalize(&mut out); + to_hex(&out) + } + + /// Hash all CHAIN_DICT entries: iterate sorted keys, + /// feed (key_be_bytes || value_byte) into keccak256. + fn hash_chain_dict() -> String { + let mut keys: Vec = CHAIN_DICT.keys().copied().collect(); + keys.sort_unstable(); + let mut k = Keccak::v256(); + for key in &keys { + k.update(&key.to_be_bytes()); + k.update(&[*CHAIN_DICT.get(key).unwrap()]); + } + let mut out = [0u8; 32]; + k.finalize(&mut out); + to_hex(&out) + } + + #[test] + fn app_dict_locked() { + // Honor VOID_DICT_OVERRIDE=1 env var to skip the assert (D-B6). + if std::env::var("VOID_DICT_OVERRIDE").as_deref() == Ok("1") { + return; + } + let actual = hash_app_dict(); + assert_eq!( + actual, APP_DICT_HASH, + "Dictionary changed! Refusing unless VOID_DICT_OVERRIDE=1.\nActual hash: {actual}" + ); + } + + #[test] + fn chain_dict_locked() { + // Honor VOID_DICT_OVERRIDE=1 env var to skip the assert (D-B6). + if std::env::var("VOID_DICT_OVERRIDE").as_deref() == Ok("1") { + return; + } + let actual = hash_chain_dict(); + assert_eq!( + actual, CHAIN_DICT_HASH, + "Dictionary changed! Refusing unless VOID_DICT_OVERRIDE=1.\nActual hash: {actual}" + ); + } + + #[test] + fn app_dict_entry_count() { + assert_eq!(APP_DICT.len(), 11, "APP_DICT must have exactly 11 entries"); + } + + #[test] + fn chain_dict_entry_count() { + assert_eq!( + CHAIN_DICT.len(), + 5, + "CHAIN_DICT must have exactly 5 entries" + ); + } + + #[test] + fn app_dict_spot_check() { + assert_eq!(APP_DICT.get("@outlook.com"), Some(&0x02u8)); + assert_eq!(APP_DICT.get("@hotmail.com"), Some(&0x0cu8)); + assert_eq!(APP_DICT.get("INV-"), Some(&0x0fu8)); + assert_eq!(APP_DICT.get(".com"), Some(&0x09u8)); + } + + #[test] + fn chain_dict_spot_check() { + assert_eq!(CHAIN_DICT.get(&1u32), Some(&0x01u8)); // Ethereum + assert_eq!(CHAIN_DICT.get(&42161u32), Some(&0x02u8)); // Arbitrum + assert_eq!(CHAIN_DICT.get(&8453u32), Some(&0x05u8)); // Base + } + + #[test] + fn keccak256_smoke() { + // Sanity: empty input keccak256 is the well-known value. + let hash = keccak256_hex(&[]); + assert_eq!( + hash, + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ); + } +} diff --git a/packages/codec/src/encode.rs b/packages/codec/src/encode.rs new file mode 100644 index 0000000..8786481 --- /dev/null +++ b/packages/codec/src/encode.rs @@ -0,0 +1,703 @@ +// Mirrors vl/app/src/features/invoice-codec/lib/encode.ts +// and vl/app/src/shared/lib/tlv-codec/{writer.ts,varint.ts}. +// +// TLV type registry constants mirror tlv-map.ts TlvType enum. +// Encoding order: sort by TLV type ascending (BTreeMap), then append domain separator last. + +use std::collections::BTreeMap; + +use crate::dict::{app::APP_DICT, chain::CHAIN_DICT}; +use crate::error::CodecError; +use crate::hash::keccak256; +use crate::tlv::write_tlv_stream; +use crate::varint::{write_bigint_varint, write_varint}; + +// --------------------------------------------------------------------------- +// TLV type numbers (mirrors tlv-map.ts TlvType) +// --------------------------------------------------------------------------- + +// Optional (odd) types +pub(crate) const TLV_TOKEN_ADDRESS: u8 = 1; +pub(crate) const TLV_CLIENT_WALLET: u8 = 3; +pub(crate) const TLV_NOTES: u8 = 5; +pub(crate) const TLV_FROM_EMAIL: u8 = 7; +pub(crate) const TLV_FROM_PHONE: u8 = 9; +pub(crate) const TLV_FROM_ADDRESS: u8 = 11; +pub(crate) const TLV_CLIENT_EMAIL: u8 = 13; +pub(crate) const TLV_CLIENT_PHONE: u8 = 15; +pub(crate) const TLV_CLIENT_ADDRESS: u8 = 17; +pub(crate) const TLV_TAX: u8 = 19; +pub(crate) const TLV_DISCOUNT: u8 = 21; +pub(crate) const TLV_DOMAIN_SEPARATOR: u8 = 31; +pub(crate) const TLV_FROM_TAX_ID: u8 = 35; +pub(crate) const TLV_CLIENT_TAX_ID: u8 = 37; + +// Required (even) types +pub(crate) const TLV_CHAIN_ID: u8 = 2; +pub(crate) const TLV_ISSUED_AT: u8 = 4; +pub(crate) const TLV_DUE_AT: u8 = 6; +pub(crate) const TLV_DECIMALS: u8 = 8; +pub(crate) const TLV_FROM_WALLET: u8 = 10; +pub(crate) const TLV_CURRENCY: u8 = 12; +pub(crate) const TLV_ITEMS: u8 = 14; +pub(crate) const TLV_FROM_NAME: u8 = 16; +pub(crate) const TLV_CLIENT_NAME: u8 = 18; +pub(crate) const TLV_SALT: u8 = 20; +pub(crate) const TLV_INVOICE_ID: u8 = 22; +pub(crate) const TLV_TOTAL: u8 = 24; + +// Wire format constants +pub(crate) const MAGIC: u8 = 0x56; // 'V' +pub(crate) const VERSION: u8 = 0x01; +/// High bit of VERSION byte signals whole-payload Brotli compression (set by JS shim). +pub(crate) const COMPRESSED_FLAG: u8 = 0x80; + +const MAX_TLV_COUNT: usize = 64; +const MAX_VALUE_SIZE: usize = 4096; +const MAX_PAYLOAD_SIZE: usize = 1481; // (2000 - 25 prefix) / 1.333 Base64url ratio + +// --------------------------------------------------------------------------- +// Private helpers +// --------------------------------------------------------------------------- + +/// Encode a UTF-8 string to bytes. +fn utf8_bytes(s: &str) -> Vec { + s.as_bytes().to_vec() +} + +/// Decode a 0x-prefixed hex address to 20 raw bytes. +fn address_to_bytes(address: &str) -> Result<[u8; 20], CodecError> { + let hex = address.strip_prefix("0x").unwrap_or(address); + if hex.len() != 40 { + return Err(CodecError::BadMagic); // reuse: bad address treated as corrupt input + } + let mut out = [0u8; 20]; + for i in 0..20 { + out[i] = + u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| CodecError::BadMagic)?; + } + Ok(out) +} + +/// Encode a u32 as 4-byte big-endian. +fn uint32_be(value: u32) -> Vec { + value.to_be_bytes().to_vec() +} + +/// Encode a u64 as LEB128 varint bytes. +fn varint_bytes(value: u64) -> Vec { + let mut buf = Vec::new(); + write_varint(value, &mut buf); + buf +} + +/// Encode a decimal integer string (BigInt) as mantissa + trailing-zeros. +/// Mirrors `writeMantissa` from varint.ts. +/// Amount domain is U256 — matches the on-chain uint256 domain and the TS BigInt reference. +fn mantissa_bytes(value_str: &str) -> Result, CodecError> { + use ruint::aliases::U256; + + let value: U256 = U256::from_str_radix(value_str, 10) + .map_err(|_| CodecError::InvalidAmount(value_str.to_string()))?; + + let mut buf = Vec::new(); + if value == U256::ZERO { + // mantissa = 0 (single 0x00 byte), zeros = 0 + write_bigint_varint(&[0], &mut buf); + buf.push(0); + return Ok(buf); + } + + let ten = U256::from(10u64); + let mut mantissa = value; + let mut zeros: u8 = 0; + while mantissa % ten == U256::ZERO { + mantissa /= ten; + zeros += 1; + } + // Write mantissa as big-endian bytes via bigint_varint + let mantissa_be: [u8; 32] = mantissa.to_be_bytes(); + write_bigint_varint(&mantissa_be, &mut buf); + buf.push(zeros); + Ok(buf) +} + +/// Apply app-level dictionary substitution (mirrors applyDict from app-dict.ts). +/// Replaces known string patterns with 1-byte control codes. +/// Longest match first — iterate entries in length-descending order. +fn apply_dict(input: &str) -> Vec { + // Sorted entries by key length descending (mirrors DICT_ENTRIES order in TS) + // APP_DICT is a phf map; we must apply longest-match-first manually. + let mut entries: Vec<(&str, u8)> = APP_DICT.entries().map(|(&k, &v)| (k, v)).collect(); + entries.sort_by(|a, b| b.0.len().cmp(&a.0.len())); + + let mut text = input.to_string(); + for (pattern, code) in &entries { + text = text.replace(pattern, &(String::from(char::from(*code)))); + } + text.into_bytes() +} + +/// Encode chain ID per chain-dict encoding scheme: +/// 0x00 — known chain (dict lookup, 2 bytes) +/// 0x01 — unknown chain (raw varint, 2+ bytes) +fn encode_chain_id(network_id: u32) -> Vec { + if let Some(&code) = CHAIN_DICT.get(&network_id) { + vec![0x00, code] + } else { + let mut buf = vec![0x01]; + write_varint(network_id as u64, &mut buf); + buf + } +} + +/// Currency symbol → dict code (mirrors CURRENCY_DICT in tlv-map.ts). Static: zero per-call alloc. +static CURRENCY_SYMBOL_TO_CODE: &[(&str, u8)] = &[ + ("USDC", 1), + ("USDT", 2), + ("DAI", 3), + ("ETH", 4), + ("WETH", 5), + ("MATIC", 6), + ("POL", 7), + ("WBTC", 8), + ("USDC.E", 9), + ("EURC", 10), + ("USDT0", 11), +]; + +/// Token address → dict code (mirrors TOKEN_DICT in tlv-map.ts). Static: zero per-call alloc. +/// WETH on Optimism and Base share address 0x4200…0006; Base gets code 43 via chain range check. +static TOKEN_ADDRESS_TO_CODE: &[(&str, u8)] = &[ + ("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 1), + ("0xdac17f958d2ee523a2206206994597c13d831ec7", 2), + ("0x6b175474e89094c44da98b954eedeac495271d0f", 3), + ("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 4), + ("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", 5), + ("0x1abaea1f7c830bd89acc67ec4af516284b1bc33c", 6), + ("0x6c96de32cea08842dcc4058c14d3aaad7fa41dee", 7), + ("0xaf88d065e77c8cc2239327c5edb3a432268e5831", 10), + ("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", 11), + ("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", 12), + ("0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", 13), + ("0x82af49447d8a07e3bd95bd0d56f35241523fbab1", 14), + ("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", 15), + ("0x0b2c639c533813f4aa9d7837caf62653d097ff85", 20), + ("0x7f5c764cbc14f9669b88837ca1490cca17c31607", 21), + ("0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", 22), + ("0x4200000000000000000000000000000000000006", 24), // op=24 by default; base=43 via chain check + ("0x68f180fcce6836688e9084f035309e29bf0a2095", 25), + ("0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", 30), + ("0x2791bca1f2de4661ed88a30c99a7a9449aa84174", 31), + ("0xc2132d05d31c914a87c6611c10748aeb04b58e8f", 32), + ("0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", 33), + ("0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", 34), + ("0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", 35), + ("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 40), + ("0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca", 41), + ("0x50c5725949a6f0c72e6c4a641f24049a917db0cb", 42), + ("0x0555e30da8f98308edb960aa94c0ed47230d2b9c", 44), + ("0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42", 45), +]; + +/// Chain ID → (code_min, code_max) range for token dict validation. +static CHAIN_CODE_RANGES: &[(u32, u8, u8)] = &[ + (1, 1, 9), + (42161, 10, 19), + (10, 20, 29), + (137, 30, 39), + (8453, 40, 49), +]; + +/// Encode currency per spec §5.1: +/// 0x00 — dict known currency +/// 0x01 — raw UTF-8 +fn encode_currency(currency: &str) -> Vec { + let upper = currency.to_uppercase(); + if let Some(&(_, code)) = CURRENCY_SYMBOL_TO_CODE + .iter() + .find(|&&(k, _)| k == upper.as_str()) + { + vec![0x00, code] + } else { + let mut val = vec![0x01]; + val.extend_from_slice(currency.as_bytes()); + val + } +} + +/// Encode a token address per spec §5.2: +/// 0x00 — dict known token +/// 0x01 <20 bytes> — raw address +fn encode_token_address(address: &str, network_id: u32) -> Result, CodecError> { + let addr_lower = address.to_lowercase(); + + if let Some(&(_, code)) = TOKEN_ADDRESS_TO_CODE + .iter() + .find(|&&(k, _)| k == addr_lower.as_str()) + { + // WETH at 0x4200…0006 is shared by Optimism (code 24) and Base (code 43). + // On Base, override to 43 so the decoder resolves the correct chain context. + let effective_code = + if addr_lower == "0x4200000000000000000000000000000000000006" && network_id == 8453 { + 43u8 + } else { + code + }; + + let in_range = CHAIN_CODE_RANGES + .iter() + .find(|&&(chain_id, _, _)| chain_id == network_id) + .map(|&(_, min, max)| effective_code >= min && effective_code <= max) + .unwrap_or(true); // unknown chain → no range restriction + + if in_range { + return Ok(vec![0x00, effective_code]); + } + } + + let raw = address_to_bytes(address)?; + let mut val = vec![0x01]; + val.extend_from_slice(&raw); + Ok(val) +} + +/// Encode items array into packed binary (Type 14, mirrors packItems from encode.ts). +/// Format: [count: varint] per item: [desc_len: varint][desc_bytes][qty: scale+varint][rate: mantissa] +fn pack_items(items: &[crate::invoice::InvoiceItem]) -> Result, CodecError> { + let mut buf = Vec::new(); + write_varint(items.len() as u64, &mut buf); + + for item in items { + // description: apply dict, then length-prefix with varint + let desc_bytes = apply_dict(&item.description); + write_varint(desc_bytes.len() as u64, &mut buf); + buf.extend_from_slice(&desc_bytes); + + // quantity: [scale: u8][scaled_value: varint] — mirrors writeQuantity + write_quantity(&mut buf, item.quantity); + + // rate: mantissa + trailing zeros — mirrors writeMantissa + let rate_bytes = mantissa_bytes(&item.rate)?; + buf.extend_from_slice(&rate_bytes); + } + Ok(buf) +} + +/// Encode a fractional quantity as [scale: u8][scaled_value: varint]. +/// Mirrors writeQuantity from varint.ts. +fn write_quantity(buf: &mut Vec, qty: f64) { + let mut scale = 0u8; + let mut scaled = qty; + while scale < 9 && (scaled.round() - scaled).abs() > 1e-9 { + scale += 1; + scaled = qty * 10f64.powi(scale as i32); + } + let scaled_int = scaled.round() as u64; + buf.push(scale); + write_varint(scaled_int, buf); +} + +/// Compute domain separator: keccak256("VOIDPAY_INVOICE_V1" || serialized TLV records except type 31). +/// Mirrors computeDomainSeparator from security.ts. +fn compute_domain_separator(records: &BTreeMap>) -> Vec { + let prefix = b"VOIDPAY_INVOICE_V1"; + let mut body: Vec = prefix.to_vec(); + + // Serialize each record except domain separator (type 31) in key-ascending order + for (&tlv_type, value) in records { + if tlv_type == TLV_DOMAIN_SEPARATOR { + continue; + } + // type(1) + length(varint) + value — mirrors TLV wire format + body.push(tlv_type); + write_varint(value.len() as u64, &mut body); + body.extend_from_slice(value); + } + + keccak256(&body).to_vec() +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// Encode an [`Invoice`] to canonical pre-compression bytes (payment identity). +/// +/// The output is the raw TLV binary: `[MAGIC][VERSION][COUNT][TLV records...]`. +/// Feed the output to `compute_content_hash()` for ERC-3009 nonce binding. +/// The COMPRESSED_FLAG (0x80) is never set — compression lives in the JS shim layer. +/// +/// # Errors +/// Returns [`CodecError`] if any field is malformed (bad address hex, invalid amount, etc.). +/// +/// # Example +/// ``` +/// use void_layer_codec::{encode_invoice_canonical, Invoice, InvoiceFrom, InvoiceClient, InvoiceItem}; +/// let invoice = Invoice { +/// invoice_id: "INV-001".to_string(), +/// issued_at: 1_700_000_000, +/// due_at: 1_700_604_800, +/// network_id: 1, +/// currency: "USDC".to_string(), +/// decimals: 6, +/// from: InvoiceFrom { +/// name: "Alice".to_string(), +/// wallet_address: "0xaabbccddee0011223344556677889900aabbccdd".to_string(), +/// email: None, phone: None, physical_address: None, tax_id: None, +/// }, +/// client: InvoiceClient { +/// name: "Bob".to_string(), +/// wallet_address: None, email: None, phone: None, +/// physical_address: None, tax_id: None, +/// }, +/// items: vec![InvoiceItem { +/// description: "Work".to_string(), quantity: 1.0, rate: "1000000".to_string(), +/// }], +/// token_address: None, notes: None, tax: None, discount: None, +/// total: "1000000".to_string(), +/// salt: "00112233445566778899aabbccddeeff".to_string(), +/// }; +/// let bytes = encode_invoice_canonical(&invoice).unwrap(); +/// assert_eq!(bytes[0], 0x56); // magic +/// assert_eq!(bytes[1], 0x01); // version (no COMPRESSED_FLAG) +/// ``` +pub fn encode_invoice_canonical(invoice: &crate::invoice::Invoice) -> Result, CodecError> { + let mut map: BTreeMap> = BTreeMap::new(); + + // --- Required fields (even TLV types) --- + + // Chain ID (type 2) + map.insert(TLV_CHAIN_ID, encode_chain_id(invoice.network_id)); + + // Issued at (type 4): uint32 BE + map.insert(TLV_ISSUED_AT, uint32_be(invoice.issued_at)); + + // Due at (type 6): delta from issuedAt as varint + let due_delta = invoice.due_at.saturating_sub(invoice.issued_at); + map.insert(TLV_DUE_AT, varint_bytes(due_delta as u64)); + + // Decimals (type 8): single byte + map.insert(TLV_DECIMALS, vec![invoice.decimals]); + + // From wallet (type 10): 20 raw bytes + let from_wallet = address_to_bytes(&invoice.from.wallet_address)?; + map.insert(TLV_FROM_WALLET, from_wallet.to_vec()); + + // Currency (type 12) + map.insert(TLV_CURRENCY, encode_currency(&invoice.currency)); + + // Items (type 14): packed binary + map.insert(TLV_ITEMS, pack_items(&invoice.items)?); + + // From name (type 16): dict-applied UTF-8 + map.insert(TLV_FROM_NAME, apply_dict(&invoice.from.name)); + + // Client name (type 18): dict-applied UTF-8 + map.insert(TLV_CLIENT_NAME, apply_dict(&invoice.client.name)); + + // Salt (type 20): decode hex string → raw bytes + let salt_bytes = hex_decode_salt(&invoice.salt)?; + map.insert(TLV_SALT, salt_bytes); + + // Invoice ID (type 22): raw UTF-8 (NOT dict-applied per encode.ts comment) + map.insert(TLV_INVOICE_ID, utf8_bytes(&invoice.invoice_id)); + + // Total (type 24): mantissa-encoded + map.insert(TLV_TOTAL, mantissa_bytes(&invoice.total)?); + + // --- Optional fields (odd TLV types) --- + + if let Some(ref addr) = invoice.token_address { + map.insert( + TLV_TOKEN_ADDRESS, + encode_token_address(addr, invoice.network_id)?, + ); + } + + if let Some(ref wallet) = invoice.client.wallet_address { + let raw = address_to_bytes(wallet)?; + map.insert(TLV_CLIENT_WALLET, raw.to_vec()); + } + + if let Some(ref notes) = invoice.notes { + map.insert(TLV_NOTES, apply_dict(notes)); + } + + if let Some(ref email) = invoice.from.email { + map.insert(TLV_FROM_EMAIL, apply_dict(email)); + } + + if let Some(ref phone) = invoice.from.phone { + map.insert(TLV_FROM_PHONE, apply_dict(phone)); + } + + if let Some(ref addr) = invoice.from.physical_address { + map.insert(TLV_FROM_ADDRESS, apply_dict(addr)); + } + + if let Some(ref tax_id) = invoice.from.tax_id { + map.insert(TLV_FROM_TAX_ID, apply_dict(tax_id)); + } + + if let Some(ref email) = invoice.client.email { + map.insert(TLV_CLIENT_EMAIL, apply_dict(email)); + } + + if let Some(ref phone) = invoice.client.phone { + map.insert(TLV_CLIENT_PHONE, apply_dict(phone)); + } + + if let Some(ref addr) = invoice.client.physical_address { + map.insert(TLV_CLIENT_ADDRESS, apply_dict(addr)); + } + + if let Some(ref tax_id) = invoice.client.tax_id { + map.insert(TLV_CLIENT_TAX_ID, apply_dict(tax_id)); + } + + if let Some(ref tax) = invoice.tax { + map.insert(TLV_TAX, utf8_bytes(tax)); + } + + if let Some(ref discount) = invoice.discount { + map.insert(TLV_DISCOUNT, utf8_bytes(discount)); + } + + // Domain separator (type 31): computed over all other records + let domain_sep = compute_domain_separator(&map); + map.insert(TLV_DOMAIN_SEPARATOR, domain_sep); + + // Validate counts and sizes + if map.len() > MAX_TLV_COUNT { + return Err(CodecError::CompressionFailed(format!( + "TLV count {} exceeds max {}", + map.len(), + MAX_TLV_COUNT + ))); + } + for value in map.values() { + if value.len() > MAX_VALUE_SIZE { + return Err(CodecError::CompressionFailed(format!( + "TLV value size {} exceeds max {}", + value.len(), + MAX_VALUE_SIZE + ))); + } + } + + // Serialize: [MAGIC][VERSION][COUNT][TLV records in type-ascending order] + let mut out = Vec::new(); + out.push(MAGIC); + out.push(VERSION); + out.push(map.len() as u8); + write_tlv_stream(&map, &mut out); + + if out.len() > MAX_PAYLOAD_SIZE { + return Err(CodecError::CompressionFailed(format!( + "payload size {} exceeds max {}", + out.len(), + MAX_PAYLOAD_SIZE + ))); + } + + Ok(out) +} + +// --------------------------------------------------------------------------- +// Private helpers (continued) +// --------------------------------------------------------------------------- + +/// Decode a 32-char hex string (16 bytes) into raw bytes for salt. +fn hex_decode_salt(hex: &str) -> Result, CodecError> { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + if hex.len() != 32 { + return Err(CodecError::CompressionFailed(format!( + "salt must be 32 hex chars (16 bytes), got {} chars", + hex.len() + ))); + } + let mut bytes = Vec::with_capacity(16); + for i in 0..16 { + bytes.push( + u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16) + .map_err(|_| CodecError::CompressionFailed("invalid salt hex".to_string()))?, + ); + } + Ok(bytes) +} + +// --------------------------------------------------------------------------- +// Test helpers (pub only under #[cfg(test)]) +// --------------------------------------------------------------------------- + +#[cfg(test)] +pub(crate) mod tests_pub { + use super::*; + + pub(crate) fn mantissa_bytes_pub(s: &str) -> Result, crate::error::CodecError> { + mantissa_bytes(s) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::invoice::InvoiceItem; + + #[test] + fn mantissa_bytes_zero() { + let b = mantissa_bytes("0").unwrap(); + // mantissa=0 → write_bigint_varint([0]) = [0x00], zeros=0 + assert_eq!(b, vec![0x00, 0x00]); + } + + #[test] + fn mantissa_bytes_one_million() { + // 1_000_000 = 1 * 10^6 → mantissa=1 (0x01), zeros=6 + let b = mantissa_bytes("1000000").unwrap(); + assert_eq!(b, vec![0x01, 0x06]); + } + + #[test] + fn mantissa_bytes_123() { + // 123 — no trailing zeros → mantissa=123, zeros=0 + // 123 = 0x7B → LEB128 single byte + let b = mantissa_bytes("123").unwrap(); + assert_eq!(b, vec![0x7b, 0x00]); + } + + #[test] + fn write_quantity_integer_one() { + let mut buf = Vec::new(); + write_quantity(&mut buf, 1.0); + // scale=0, value=1 → [0x00, 0x01] + assert_eq!(buf, vec![0x00, 0x01]); + } + + #[test] + fn write_quantity_1_5() { + let mut buf = Vec::new(); + write_quantity(&mut buf, 1.5); + // scale=1, value=15 → [0x01, 0x0F] + assert_eq!(buf, vec![0x01, 0x0F]); + } + + #[test] + fn encode_chain_id_known_ethereum() { + let b = encode_chain_id(1); + assert_eq!(b, vec![0x00, 0x01]); + } + + #[test] + fn encode_chain_id_unknown() { + let b = encode_chain_id(999999); + assert_eq!(b[0], 0x01, "unknown chain prefix must be 0x01"); + assert!(b.len() > 1, "must include varint after prefix"); + } + + #[test] + fn encode_currency_known_usdc() { + let b = encode_currency("USDC"); + assert_eq!(b, vec![0x00, 0x01]); + } + + #[test] + fn encode_currency_unknown() { + let b = encode_currency("XYZ"); + assert_eq!(b[0], 0x01); + assert_eq!(&b[1..], b"XYZ"); + } + + #[test] + fn address_to_bytes_valid() { + let b = address_to_bytes("0xaabbccddee0011223344556677889900aabbccdd").unwrap(); + assert_eq!(b[0], 0xaa); + assert_eq!(b[1], 0xbb); + assert_eq!(b[19], 0xdd); + } + + #[test] + fn pack_items_single_item() { + let items = vec![InvoiceItem { + description: "Work".to_string(), + quantity: 1.0, + rate: "1000000".to_string(), + }]; + let b = pack_items(&items).unwrap(); + // count = 1 (varint 0x01) + assert_eq!(b[0], 0x01); + } + + #[test] + fn apply_dict_substitutes_pattern() { + let result = apply_dict("Invoice total"); + // "Invoice" → 0x06 + assert_eq!(result[0], 0x06); + } + + #[test] + fn apply_dict_no_match_passthrough() { + let result = apply_dict("Hello world"); + assert_eq!(result, b"Hello world"); + } + + // --- U256 amount domain tests --- + + #[test] + fn mantissa_bytes_u128_max() { + // u128::MAX = 340282366920938463463374607431768211455 + // Must produce byte-identical output to the old u128 path. + let s = u128::MAX.to_string(); + let b = mantissa_bytes(&s).unwrap(); + // Verify encode→decode roundtrip produces the same string. + // Spot-check: no trailing zeros, so zeros byte = 0. + assert_eq!(*b.last().unwrap(), 0u8, "u128::MAX has no trailing zeros"); + } + + #[test] + fn mantissa_bytes_u256_max_roundtrips() { + // 2^256 - 1 as decimal + let uint256_max = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; + let b = mantissa_bytes(uint256_max).unwrap(); + // Last byte = trailing zeros count (should be 0 — u256::MAX is odd) + assert_eq!(*b.last().unwrap(), 0u8); + // Verify the encoded bytes decode back (via decode_mantissa) + let decoded = crate::decode::tests_pub::decode_mantissa_pub(&b).unwrap(); + assert_eq!(decoded, uint256_max); + } + + #[test] + fn mantissa_bytes_large_round_value() { + // 10^30 — large round value well above u128::MAX range in theory but fits U256 + let s = "1".to_string() + &"0".repeat(30); + let b = mantissa_bytes(&s).unwrap(); + // mantissa = 1, zeros = 30 + assert_eq!(*b.last().unwrap(), 30u8); + } + + #[test] + fn mantissa_bytes_above_u256_errors() { + // 2^256 — one above U256::MAX + let over = "115792089237316195423570985008687907853269984665640564039457584007913129639936"; + let err = mantissa_bytes(over).unwrap_err(); + assert!( + matches!(err, crate::error::CodecError::InvalidAmount(_)), + "expected InvalidAmount, got {err:?}" + ); + } + + #[test] + fn mantissa_bytes_non_numeric_errors() { + let err = mantissa_bytes("not_a_number").unwrap_err(); + assert!( + matches!(err, crate::error::CodecError::InvalidAmount(_)), + "expected InvalidAmount, got {err:?}" + ); + } +} diff --git a/packages/codec/src/error.rs b/packages/codec/src/error.rs new file mode 100644 index 0000000..281c871 --- /dev/null +++ b/packages/codec/src/error.rs @@ -0,0 +1,26 @@ +use thiserror::Error; + +/// Errors produced by the codec. Never panics on user input. +#[derive(Debug, Error)] +pub enum CodecError { + #[error("varint overflow at offset {0}")] + VarintOverflow(usize), + #[error("truncated payload: needed {needed} bytes, had {had}")] + Truncated { needed: usize, had: usize }, + #[error("unknown extension TLV type {0}")] + UnknownExtension(u8), + #[error("dictionary mismatch: expected {expected}, actual {actual}")] + DictionaryMismatch { expected: u8, actual: u8 }, + #[error("signature invalid")] + SignatureInvalid, + #[error("unsupported version {0}")] + UnsupportedVersion(u8), + #[error("bad magic bytes")] + BadMagic, + #[error("checksum mismatch")] + ChecksumMismatch, + #[error("compression failed: {0}")] + CompressionFailed(String), + #[error("invalid amount: {0}")] + InvalidAmount(String), +} diff --git a/packages/codec/src/hash.rs b/packages/codec/src/hash.rs new file mode 100644 index 0000000..b06386d --- /dev/null +++ b/packages/codec/src/hash.rs @@ -0,0 +1,71 @@ +// Dead-code lint suppressed: keccak256 is the internal primitive consumed by +// compute_content_hash and future Phase 2B codec entry-point callers. +#![allow(dead_code)] + +use tiny_keccak::{Hasher, Keccak}; + +/// Keccak-256 over `bytes`. Returns the 32-byte digest. +pub(crate) fn keccak256(bytes: &[u8]) -> [u8; 32] { + let mut k = Keccak::v256(); + k.update(bytes); + let mut out = [0u8; 32]; + k.finalize(&mut out); + out +} + +/// Compute the content hash for ERC-3009 nonce binding (spec §0.2). +/// +/// Input MUST be the canonical pre-compression binary (the TLV form), NOT wire bytes. +/// +/// # Example +/// ``` +/// use void_layer_codec::compute_content_hash; +/// let hash = compute_content_hash(b"hello"); +/// assert_eq!(hash.len(), 32); +/// ``` +pub fn compute_content_hash(canonical_binary: &[u8]) -> [u8; 32] { + keccak256(canonical_binary) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hex_to_bytes(hex: &str) -> [u8; 32] { + let mut out = [0u8; 32]; + for i in 0..32 { + out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap(); + } + out + } + + #[test] + fn keccak256_empty() { + // keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 + let expected = + hex_to_bytes("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + assert_eq!(keccak256(b""), expected); + } + + #[test] + fn keccak256_abc() { + // keccak256("abc") = 4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45 + let expected = + hex_to_bytes("4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"); + assert_eq!(keccak256(b"abc"), expected); + } + + #[test] + fn compute_content_hash_stable() { + // Hand-crafted canonical TLV sample: tag=0x01, length=0x03, value=[0xAA, 0xBB, 0xCC] + let canonical_binary: &[u8] = &[0x01, 0x03, 0xAA, 0xBB, 0xCC]; + let hash1 = compute_content_hash(canonical_binary); + let hash2 = compute_content_hash(canonical_binary); + // Deterministic: same input always yields same 32-byte digest + assert_eq!(hash1, hash2); + assert_eq!(hash1.len(), 32); + // Different input yields different digest + let other = compute_content_hash(&[0x01, 0x03, 0xAA, 0xBB, 0xCD]); + assert_ne!(hash1, other); + } +} diff --git a/packages/codec/src/index.test.ts b/packages/codec/src/index.test.ts new file mode 100644 index 0000000..325b5df --- /dev/null +++ b/packages/codec/src/index.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from 'vitest' +import { + encodeInvoiceCanonical, + decodeInvoiceCanonical, + encodeInvoiceWire, + decodeInvoiceWire, + receiptHash, +} from './index.js' + +interface DecodedInvoice { + invoice_id: string + currency: string + total: string + decimals: number +} + +const MINIMAL_INVOICE = { + invoice_id: 'INV-001', + issued_at: 1_700_000_000, + due_at: 1_700_086_400, + network_id: 8453, + currency: 'USDC', + decimals: 6, + from: { + name: 'Alice', + wallet_address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + }, + client: { name: 'Bob' }, + items: [ + { description: 'Consulting', quantity: 1.0, rate: '1000000' }, + ], + total: '1000000', + salt: 'deadbeefdeadbeefdeadbeefdeadbeef', +} + +// A larger invoice whose body Brotli can beneficially compress. Minimal +// invoices are too small for Brotli (it expands payloads <~180 B per the +// T-P2-0a spike), so the COMPRESSED_FLAG path needs a sizeable, repetitive +// payload to exercise. +const LONG_DESC = + 'Professional consulting services rendered including architecture review, ' + + 'code review, deployment support and incident response, billed monthly. ' +const LARGE_INVOICE = { + ...MINIMAL_INVOICE, + invoice_id: 'INV-LARGE-001', + items: [ + { description: LONG_DESC.repeat(3), quantity: 1.0, rate: '1000000' }, + { description: LONG_DESC.repeat(3), quantity: 2.0, rate: '2000000' }, + { description: LONG_DESC.repeat(3), quantity: 3.0, rate: '3000000' }, + ], + total: '14000000', +} + +describe('encodeInvoiceCanonical + decodeInvoiceCanonical (WASM pass-through)', () => { + it('returns Uint8Array with magic byte 0x56', () => { + const bytes = encodeInvoiceCanonical(MINIMAL_INVOICE) + expect(bytes).toBeInstanceOf(Uint8Array) + expect(bytes[0]).toBe(0x56) + }) + + it('roundtrips through canonical encode → decode', () => { + const bytes = encodeInvoiceCanonical(MINIMAL_INVOICE) + const decoded = decodeInvoiceCanonical(bytes) as DecodedInvoice + expect(decoded.invoice_id).toBe('INV-001') + expect(decoded.currency).toBe('USDC') + expect(decoded.total).toBe('1000000') + }) +}) + +describe('encodeInvoiceWire', () => { + it('sets COMPRESSED_FLAG (0x80) on version byte when compression is beneficial', async () => { + const wire = await encodeInvoiceWire(LARGE_INVOICE) + expect(wire).toBeInstanceOf(Uint8Array) + // magic byte preserved + expect(wire[0]).toBe(0x56) + // version byte must have 0x80 set (Brotli compressed) + expect(wire[1]! & 0x80).toBe(0x80) + // compressed wire must be smaller than the canonical bytes + expect(wire.length).toBeLessThan(encodeInvoiceCanonical(LARGE_INVOICE).length) + }) + + it('falls back to uncompressed (flag clear) when Brotli would expand the payload', async () => { + // A minimal invoice is too small for Brotli to help — the shim must emit + // the uncompressed canonical bytes with COMPRESSED_FLAG clear. + const wire = await encodeInvoiceWire(MINIMAL_INVOICE) + expect(wire[1]! & 0x80).toBe(0) + // and it must still roundtrip through decodeInvoiceWire + const decoded = (await decodeInvoiceWire(wire)) as DecodedInvoice + expect(decoded.invoice_id).toBe('INV-001') + }) +}) + +describe('decodeInvoiceWire', () => { + it('roundtrips through wire encode → decode', async () => { + const wire = await encodeInvoiceWire(MINIMAL_INVOICE) + const decoded = (await decodeInvoiceWire(wire)) as DecodedInvoice + expect(decoded.invoice_id).toBe('INV-001') + expect(decoded.currency).toBe('USDC') + expect(decoded.total).toBe('1000000') + expect(decoded.decimals).toBe(6) + }) + + it('accepts uncompressed canonical bytes (flag clear path)', async () => { + // decodeInvoiceWire must handle uncompressed input (flag not set) + const canonical = encodeInvoiceCanonical(MINIMAL_INVOICE) + // Verify flag is NOT set on canonical output + expect(canonical[1]! & 0x80).toBe(0) + // decodeInvoiceWire should pass through to canonical decode + const decoded = (await decodeInvoiceWire(canonical)) as DecodedInvoice + expect(decoded.invoice_id).toBe('INV-001') + }) +}) + +describe('receiptHash (JS export coverage)', () => { + // Hand-crafted canonical TLV: tag=0x01, length=0x03, value=[0xAA, 0xBB, 0xCC] + const CANONICAL_FIXTURE = new Uint8Array([0x01, 0x03, 0xaa, 0xbb, 0xcc]) + + it('returns a 32-byte Uint8Array and is deterministic', () => { + const first = receiptHash(CANONICAL_FIXTURE) + const second = receiptHash(CANONICAL_FIXTURE) + expect(first).toBeInstanceOf(Uint8Array) + expect(first).toHaveLength(32) + expect(first).toEqual(second) + }) +}) diff --git a/packages/codec/src/index.ts b/packages/codec/src/index.ts new file mode 100644 index 0000000..5fa04be --- /dev/null +++ b/packages/codec/src/index.ts @@ -0,0 +1,97 @@ +/** + * @void-layer/codec JS shim — public entry point. + * + * Exposes 5 functions: + * - encodeInvoiceCanonical / decodeInvoiceCanonical (WASM canonical, no Brotli) + * - receiptHash (keccak-256 of canonical bytes) + * - encodeInvoiceWire / decodeInvoiceWire (Brotli-compressed wire format) + * + * Brotli compression is handled here via `brotli-wasm` peerDependency. + * COMPRESSED_FLAG logic mirrors vl/app/src/shared/lib/tlv-codec/compress.ts §compressPayload. + */ + +import type { BrotliWasmType } from 'brotli-wasm' + +// Re-export canonical WASM functions directly. +export { + encodeInvoiceCanonical, + decodeInvoiceCanonical, + receiptHash, +} from '../pkg/void_layer_codec.js' + +// --------------------------------------------------------------------------- +// Brotli lazy init (mirrors compressPayload reference pattern) +// --------------------------------------------------------------------------- + +const COMPRESSED_FLAG = 0x80 + +let _brotli: BrotliWasmType | null = null + +async function getBrotli(): Promise { + if (!_brotli) { + const mod = await import('brotli-wasm') + const instance = await mod.default + _brotli = instance + } + return _brotli +} + +// --------------------------------------------------------------------------- +// Wire encode — MAGIC + (VERSION | COMPRESSED_FLAG) + brotli(body) +// Falls back to uncompressed if Brotli expands the payload. +// +// Input: invoice object (same shape as encodeInvoiceCanonical) +// Output: [MAGIC][VERSION | 0x80][brotli([COUNT][TLV records...])] +// OR uncompressed canonical bytes if Brotli would expand. +// +// Mirrors: compressPayload() in tlv-codec/compress.ts +// --------------------------------------------------------------------------- + +export async function encodeInvoiceWire(invoice: unknown): Promise { + const { encodeInvoiceCanonical: encodeCanonical } = await import( + '../pkg/void_layer_codec.js' + ) + const canonical: Uint8Array = encodeCanonical(invoice) + + if (canonical.length < 3) return canonical + + const brotli = await getBrotli() + const body = canonical.slice(2) // [COUNT][TLV records...] + const compressed = brotli.compress(body, { quality: 11 }) + + if (compressed.length >= body.length) return canonical + + const result = new Uint8Array(2 + compressed.length) + result[0] = canonical[0]! // MAGIC + result[1] = canonical[1]! | COMPRESSED_FLAG // VERSION | 0x80 + result.set(compressed, 2) + return result +} + +// --------------------------------------------------------------------------- +// Wire decode — detects COMPRESSED_FLAG and decompresses if set. +// Accepts both compressed wire bytes and uncompressed canonical bytes. +// +// Mirrors: decompressPayload() in tlv-codec/compress.ts +// --------------------------------------------------------------------------- + +export async function decodeInvoiceWire(bytes: Uint8Array): Promise { + const { decodeInvoiceCanonical: decodeCanonical } = await import( + '../pkg/void_layer_codec.js' + ) + + if (bytes.length < 3 || !(bytes[1]! & COMPRESSED_FLAG)) { + return decodeCanonical(bytes) + } + + const brotli = await getBrotli() + const compressedBody = bytes.slice(2) + const decompressed = brotli.decompress(compressedBody) + + const canonical = new Uint8Array(2 + decompressed.length) + canonical[0] = bytes[0]! // MAGIC + canonical[1] = bytes[1]! & 0x7f // VERSION without COMPRESSED_FLAG + canonical.set(decompressed, 2) + + return decodeCanonical(canonical) +} diff --git a/packages/codec/src/invoice.rs b/packages/codec/src/invoice.rs new file mode 100644 index 0000000..dd0ea77 --- /dev/null +++ b/packages/codec/src/invoice.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; + +/// A single line item in an invoice. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InvoiceItem { + /// Human-readable description of the line item. + pub description: String, + /// Quantity (may be fractional, e.g. 1.5 hours). + pub quantity: f64, + /// Unit rate in atomic token units (BigInt-safe string, e.g. "1000000" for 1 USDC). + pub rate: String, +} + +/// Originator (payee) contact details. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InvoiceFrom { + /// Display name of the issuer. + pub name: String, + /// EVM wallet address (0x-prefixed hex). + pub wallet_address: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub phone: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub physical_address: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tax_id: Option, +} + +/// Client (payer) contact details. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InvoiceClient { + /// Display name of the client. + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wallet_address: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub phone: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub physical_address: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tax_id: Option, +} + +/// Canonical invoice data structure (v1 schema, LOCKED). +/// +/// All monetary amounts are represented as `String` for BigInt-safe JS boundary +/// (D-B11). Amounts are in atomic token units (e.g. USDC uses 6 decimals). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Invoice { + /// Unique invoice identifier (e.g. "INV-001"). + pub invoice_id: String, + /// Unix timestamp of invoice creation (seconds). + pub issued_at: u32, + /// Unix timestamp of payment due date (seconds). + pub due_at: u32, + /// EVM chain ID (e.g. 1 = Ethereum, 8453 = Base). + pub network_id: u32, + /// Token currency symbol (e.g. "USDC", "ETH"). + pub currency: String, + /// Token decimals (e.g. 6 for USDC, 18 for ETH). + pub decimals: u8, + /// Issuer details (name, wallet address, optional contact info). + pub from: InvoiceFrom, + /// Client/payer details. + pub client: InvoiceClient, + /// Line items. + pub items: Vec, + /// ERC-20 token contract address (None for native ETH/MATIC). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_address: Option, + /// Payment notes (max 280 chars). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub notes: Option, + /// Tax percentage as string (e.g. "10.5"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tax: Option, + /// Discount percentage as string (e.g. "5"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub discount: Option, + /// Total payment amount in atomic units (BigInt-safe string). Includes magic dust if applied. + pub total: String, + /// 16-byte random salt for magic dust and domain separator (hex string). + /// Caller provides this; encoder uses it as-is for deterministic re-encoding. + pub salt: String, +} diff --git a/packages/codec/src/lib.rs b/packages/codec/src/lib.rs new file mode 100644 index 0000000..04ef3cc --- /dev/null +++ b/packages/codec/src/lib.rs @@ -0,0 +1,39 @@ +//! @void-layer/codec — canonical Invoice codec. +//! +//! TLV wire format + keccak256 content hash. Brotli compression lives +//! in the JS shim layer (`src/index.ts`) over the `brotli-wasm` peerDep +//! per B-v replan (2026-05-20). +//! +//! # Public API (B-v — canonical only in Rust) +//! +//! ```text +//! encode_invoice_canonical → canonical TLV bytes (pre-compression, payment identity) +//! decode_invoice_canonical → Invoice from canonical bytes +//! compute_content_hash → keccak256 of canonical bytes (ERC-3009 nonce) +//! ``` +//! +//! Wire encoding (Brotli + COMPRESSED_FLAG) is provided by the JS shim +//! (`encodeInvoiceWire` / `decodeInvoiceWire`) which wraps these fns and +//! calls `brotli-wasm` as a peerDep. +//! +//! See spec 056 in voidpay-ai for full design. + +pub mod error; +pub mod invoice; + +pub(crate) mod decode; +pub(crate) mod dict; +pub(crate) mod encode; +pub(crate) mod hash; +pub(crate) mod tlv; +pub(crate) mod varint; + +#[cfg(target_arch = "wasm32")] +mod wasm; + +// --- Canonical public surface --- +pub use decode::decode_invoice_canonical; +pub use encode::encode_invoice_canonical; +pub use error::CodecError; +pub use hash::compute_content_hash; +pub use invoice::{Invoice, InvoiceClient, InvoiceFrom, InvoiceItem}; diff --git a/packages/codec/src/tlv.rs b/packages/codec/src/tlv.rs new file mode 100644 index 0000000..fe17b33 --- /dev/null +++ b/packages/codec/src/tlv.rs @@ -0,0 +1,277 @@ +// Dead-code lint suppressed: these pub(crate) functions are the Phase 2A wire-format +// API consumed by the encode/decode entry-point landing in Phase 2B+. +#![allow(dead_code)] + +use std::collections::BTreeMap; + +use crate::error::CodecError; +use crate::varint::{read_varint, write_varint}; + +/// A single TLV (Type-Length-Value) record. +#[derive(Debug)] +pub(crate) struct TlvRecord { + pub tlv_type: u8, + pub value: Vec, +} + +/// Reads one TLV record from `buf` starting at `offset`. +/// +/// Returns `(record, bytes_consumed)`. +/// +/// Wire format: `TYPE(1) | LENGTH(LEB128) | VALUE(length bytes)`. +/// +/// Errors: +/// - `CodecError::Truncated` if the buffer ends before the type byte or mid-value. +pub(crate) fn read_tlv(buf: &[u8], offset: usize) -> Result<(TlvRecord, usize), CodecError> { + if offset >= buf.len() { + return Err(CodecError::Truncated { + needed: offset + 1, + had: buf.len(), + }); + } + let tlv_type = buf[offset]; + let mut consumed = 1usize; + + let (length, varint_bytes) = read_varint(buf, offset + consumed)?; + consumed += varint_bytes; + + let length = length as usize; + let value_end = offset + consumed + length; + if value_end > buf.len() { + return Err(CodecError::Truncated { + needed: value_end, + had: buf.len(), + }); + } + let value = buf[offset + consumed..value_end].to_vec(); + consumed += length; + + Ok((TlvRecord { tlv_type, value }, consumed)) +} + +/// Serializes one TLV record into `out`. +/// +/// Wire format: `TYPE(1) | LENGTH(LEB128) | VALUE`. +pub(crate) fn write_tlv(record: &TlvRecord, out: &mut Vec) { + out.push(record.tlv_type); + write_varint(record.value.len() as u64, out); + out.extend_from_slice(&record.value); +} + +/// Reads a flat sequence of TLV records from `buf` (the entire slice). +/// +/// Returns a `BTreeMap`. Duplicate types are last-write-wins +/// (matches TS reader behaviour — the stream is trusted to be canonical). +/// +/// Errors: propagated from `read_tlv`. +pub(crate) fn read_tlv_stream(buf: &[u8]) -> Result>, CodecError> { + let mut map = BTreeMap::new(); + let mut offset = 0; + while offset < buf.len() { + let (record, consumed) = read_tlv(buf, offset)?; + map.insert(record.tlv_type, record.value); + offset += consumed; + } + Ok(map) +} + +/// Serializes a `BTreeMap` of TLV entries into `out` in key order. +/// +/// `BTreeMap` guarantees ascending key iteration, so output is deterministic +/// (D-B4: byte-stable encoding requires deterministic field ordering). +pub(crate) fn write_tlv_stream(stream: &BTreeMap>, out: &mut Vec) { + for (&tlv_type, value) in stream { + let record = TlvRecord { + tlv_type, + value: value.clone(), + }; + write_tlv(&record, out); + } +} + +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // --- read_tlv / write_tlv single-record roundtrip ---------------------- + + #[test] + fn single_record_roundtrip() { + let record = TlvRecord { + tlv_type: 0x01, + value: vec![0xAA, 0xBB, 0xCC], + }; + let mut buf = Vec::new(); + write_tlv(&record, &mut buf); + + // Wire: [0x01, 0x03, 0xAA, 0xBB, 0xCC] + assert_eq!(buf, vec![0x01, 0x03, 0xAA, 0xBB, 0xCC]); + + let (decoded, consumed) = read_tlv(&buf, 0).unwrap(); + assert_eq!(decoded.tlv_type, 0x01); + assert_eq!(decoded.value, vec![0xAA, 0xBB, 0xCC]); + assert_eq!(consumed, 5); + } + + #[test] + fn empty_value_record_roundtrip() { + let record = TlvRecord { + tlv_type: 0xFF, + value: vec![], + }; + let mut buf = Vec::new(); + write_tlv(&record, &mut buf); + + // Wire: [0xFF, 0x00] + assert_eq!(buf, vec![0xFF, 0x00]); + + let (decoded, consumed) = read_tlv(&buf, 0).unwrap(); + assert_eq!(decoded.tlv_type, 0xFF); + assert_eq!(decoded.value, vec![]); + assert_eq!(consumed, 2); + } + + #[test] + fn large_value_uses_multi_byte_varint_length() { + // 128-byte value → length encoded as two LEB128 bytes [0x80, 0x01] + let value = vec![0u8; 128]; + let record = TlvRecord { + tlv_type: 0x02, + value: value.clone(), + }; + let mut buf = Vec::new(); + write_tlv(&record, &mut buf); + + // TYPE(1) + LENGTH(2) + VALUE(128) = 131 bytes + assert_eq!(buf.len(), 131); + assert_eq!(buf[0], 0x02); + assert_eq!(&buf[1..3], &[0x80, 0x01]); // LEB128(128) = [0x80, 0x01] + + let (decoded, consumed) = read_tlv(&buf, 0).unwrap(); + assert_eq!(decoded.tlv_type, 0x02); + assert_eq!(decoded.value, value); + assert_eq!(consumed, 131); + } + + // --- read_tlv_stream / write_tlv_stream multi-record roundtrip ---------- + + #[test] + fn stream_roundtrip_multi_record() { + let mut stream = BTreeMap::new(); + stream.insert(0x01u8, vec![0x11u8, 0x22]); + stream.insert(0x02u8, vec![0x33u8]); + stream.insert(0x05u8, vec![0xAAu8, 0xBBu8, 0xCC]); + + let mut buf = Vec::new(); + write_tlv_stream(&stream, &mut buf); + + let decoded = read_tlv_stream(&buf).unwrap(); + assert_eq!(decoded, stream); + } + + #[test] + fn stream_empty_map_produces_empty_bytes() { + let stream: BTreeMap> = BTreeMap::new(); + let mut buf = Vec::new(); + write_tlv_stream(&stream, &mut buf); + assert!(buf.is_empty()); + + let decoded = read_tlv_stream(&buf).unwrap(); + assert!(decoded.is_empty()); + } + + // --- byte-stability invariant ------------------------------------------ + + #[test] + fn write_tlv_stream_is_byte_stable_across_two_runs() { + let mut stream = BTreeMap::new(); + stream.insert(0x03u8, vec![0x01u8, 0x02, 0x03]); + stream.insert(0x01u8, vec![0xFFu8]); + stream.insert(0x02u8, vec![0x00u8, 0x00]); + + let mut buf1 = Vec::new(); + write_tlv_stream(&stream, &mut buf1); + + let mut buf2 = Vec::new(); + write_tlv_stream(&stream, &mut buf2); + + assert_eq!(buf1, buf2, "write_tlv_stream must be byte-stable"); + } + + #[test] + fn write_tlv_stream_key_order_is_ascending() { + // Insert in reverse order; BTreeMap must emit in key-ascending order. + let mut stream = BTreeMap::new(); + stream.insert(0x05u8, vec![0x55u8]); + stream.insert(0x01u8, vec![0x11u8]); + stream.insert(0x03u8, vec![0x33u8]); + + let mut buf = Vec::new(); + write_tlv_stream(&stream, &mut buf); + + // First type byte in wire output must be 0x01 (lowest key). + assert_eq!(buf[0], 0x01, "first emitted type should be the lowest key"); + } + + // --- Truncated errors --------------------------------------------------- + + #[test] + fn truncated_on_empty_buffer() { + let err = read_tlv(&[], 0).unwrap_err(); + assert!( + matches!(err, CodecError::Truncated { .. }), + "expected Truncated, got {err:?}" + ); + } + + #[test] + fn truncated_when_value_bytes_missing() { + // TYPE=0x01, LENGTH=0x03 (3 bytes), but only 1 value byte present. + let buf = &[0x01u8, 0x03, 0xAA]; + let err = read_tlv(buf, 0).unwrap_err(); + assert!( + matches!(err, CodecError::Truncated { needed: 5, had: 3 }), + "expected Truncated{{needed:5, had:3}}, got {err:?}" + ); + } + + #[test] + fn truncated_when_type_byte_at_offset_beyond_buf() { + let buf = &[0x01u8, 0x01, 0xAAu8]; // valid single record, 3 bytes + let err = read_tlv(buf, 3).unwrap_err(); // offset == buf.len() + assert!( + matches!(err, CodecError::Truncated { .. }), + "expected Truncated, got {err:?}" + ); + } + + #[test] + fn truncated_mid_stream_surfaces_error() { + // Write a valid two-record stream, then truncate the second record's value. + let mut good_buf = Vec::new(); + write_tlv( + &TlvRecord { + tlv_type: 0x01, + value: vec![0x01], + }, + &mut good_buf, + ); + write_tlv( + &TlvRecord { + tlv_type: 0x02, + value: vec![0xAA, 0xBB, 0xCC], + }, + &mut good_buf, + ); + + // Truncate: drop the last byte of the second record's value. + let truncated = &good_buf[..good_buf.len() - 1]; + let err = read_tlv_stream(truncated).unwrap_err(); + assert!( + matches!(err, CodecError::Truncated { .. }), + "expected Truncated from stream, got {err:?}" + ); + } +} diff --git a/packages/codec/src/varint.rs b/packages/codec/src/varint.rs new file mode 100644 index 0000000..d8c058d --- /dev/null +++ b/packages/codec/src/varint.rs @@ -0,0 +1,296 @@ +// Dead-code lint suppressed: these pub(crate) functions are the Phase 2A wire-format +// API consumed by the TLV layer and codec entry-point landing in Phase 2B+. +#![allow(dead_code)] + +use crate::error::CodecError; + +/// Maximum LEB128 bytes allowed per value. +/// ceil(256 / 7) = 37 — covers uint256 with margin (spec §3.15). +pub(crate) const MAX_BYTES: usize = 37; + +/// Encodes a `u64` as LEB128 into `out`. +pub(crate) fn write_varint(value: u64, out: &mut Vec) { + let mut v = value; + loop { + let byte = (v & 0x7F) as u8; + v >>= 7; + if v == 0 { + out.push(byte); + break; + } else { + out.push(byte | 0x80); + } + } +} + +/// Decodes a LEB128-encoded `u64` from `buf` starting at `offset`. +/// +/// Returns `(value, bytes_consumed)`. +/// +/// Errors: +/// - `CodecError::Truncated` if the buffer ends mid-varint. +/// - `CodecError::VarintOverflow` if continuation bytes exceed `MAX_BYTES`. +pub(crate) fn read_varint(buf: &[u8], offset: usize) -> Result<(u64, usize), CodecError> { + let mut value: u64 = 0; + let mut shift: u32 = 0; + let mut bytes_read: usize = 0; + + loop { + if bytes_read >= MAX_BYTES { + return Err(CodecError::VarintOverflow(offset)); + } + let pos = offset + bytes_read; + if pos >= buf.len() { + return Err(CodecError::Truncated { + needed: pos + 1, + had: buf.len(), + }); + } + let byte = buf[pos]; + bytes_read += 1; + + // Guard: shift >= 64 means this value cannot fit in a u64. + // Must precede the left-shift to prevent overflow. + if shift >= 64 { + return Err(CodecError::VarintOverflow(offset)); + } + let data = (byte & 0x7F) as u64; + value |= data << shift; + if byte & 0x80 == 0 { + break; + } + shift += 7; + } + + Ok((value, bytes_read)) +} + +/// Encodes an arbitrary-precision unsigned integer (big-endian byte slice) as LEB128 into `out`. +/// +/// `value` is interpreted as a big-endian unsigned integer. +/// An empty slice or all-zero slice encodes as a single `0x00` byte. +pub(crate) fn write_bigint_varint(value: &[u8], out: &mut Vec) { + // Strip leading zero bytes to find the canonical representation. + let value = strip_leading_zeros(value); + + if value.is_empty() { + out.push(0); + return; + } + + // Work on a mutable little-endian byte copy for bit-shifting. + let mut le = to_le_bytes(value); + + loop { + let low7 = le[0] & 0x7F; + shr7_le(&mut le); + if is_zero_le(&le) { + out.push(low7); + break; + } else { + out.push(low7 | 0x80); + } + } +} + +/// Decodes a LEB128-encoded arbitrary-precision unsigned integer from `buf` at `offset`. +/// +/// Returns `(big_endian_bytes, bytes_consumed)`. +/// +/// Errors: +/// - `CodecError::Truncated` if buffer ends mid-varint. +/// - `CodecError::VarintOverflow` if continuation bytes exceed `MAX_BYTES`. +pub(crate) fn read_bigint_varint( + buf: &[u8], + offset: usize, +) -> Result<(Vec, usize), CodecError> { + // Collect LEB128 bytes, then reconstruct the big integer. + let mut le_chunks: Vec = Vec::new(); // 7-bit chunks, little-endian order + let mut bytes_read: usize = 0; + + loop { + if bytes_read >= MAX_BYTES { + return Err(CodecError::VarintOverflow(offset)); + } + let pos = offset + bytes_read; + if pos >= buf.len() { + return Err(CodecError::Truncated { + needed: pos + 1, + had: buf.len(), + }); + } + let byte = buf[pos]; + bytes_read += 1; + le_chunks.push(byte & 0x7F); + if byte & 0x80 == 0 { + break; + } + } + + // Reconstruct the integer from 7-bit LE chunks into a LE byte array, + // then convert to big-endian. + let total_bits = le_chunks.len() * 7; + let byte_count = (total_bits + 7) / 8; + let mut result_le = vec![0u8; byte_count]; + + let mut bit_pos: usize = 0; + for chunk in &le_chunks { + let bits = *chunk as u16; + let byte_idx = bit_pos / 8; + let bit_off = (bit_pos % 8) as u16; + + if byte_idx < result_le.len() { + result_le[byte_idx] |= ((bits << bit_off) & 0xFF) as u8; + } + if bit_off > 1 && byte_idx + 1 < result_le.len() { + result_le[byte_idx + 1] |= (bits >> (8 - bit_off)) as u8; + } + bit_pos += 7; + } + + // Convert to big-endian and strip leading zeros. + result_le.reverse(); + let result = strip_leading_zeros(&result_le).to_vec(); + + // An empty result means zero — return a single zero byte. + if result.is_empty() { + return Ok((vec![0], bytes_read)); + } + + Ok((result, bytes_read)) +} + +// --- Private helpers ------------------------------------------------------- + +fn strip_leading_zeros(bytes: &[u8]) -> &[u8] { + let start = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len()); + &bytes[start..] +} + +/// Convert big-endian byte slice to a little-endian Vec. +fn to_le_bytes(be: &[u8]) -> Vec { + let mut le = be.to_vec(); + le.reverse(); + le +} + +/// Right-shift a little-endian byte array by 7 bits in place. +fn shr7_le(le: &mut Vec) { + let mut carry: u16 = 0; + for b in le.iter_mut().rev() { + let val = (*b as u16) | (carry << 8); + *b = (val >> 7) as u8; + carry = val & 0x7F; + } + // Trim trailing zero bytes (which are the most-significant in LE). + while le.len() > 1 && le[le.len() - 1] == 0 { + le.pop(); + } +} + +fn is_zero_le(le: &[u8]) -> bool { + le.iter().all(|&b| b == 0) +} + +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn writes_zero_as_single_byte_zero() { + let mut buf = Vec::new(); + write_varint(0, &mut buf); + assert_eq!(buf, &[0x00]); + } + + #[test] + fn writes_127_as_single_byte() { + let mut buf = Vec::new(); + write_varint(127, &mut buf); + assert_eq!(buf, &[0x7F]); + } + + #[test] + fn writes_128_with_continuation_bit() { + let mut buf = Vec::new(); + write_varint(128, &mut buf); + // 128 = 0b10000000 → LEB128: [0x80, 0x01] + assert_eq!(buf, &[0x80, 0x01]); + } + + #[test] + fn returns_truncated_error_on_short_buffer() { + // A byte with continuation bit set but no following byte. + let buf = &[0x80u8]; + let err = read_varint(buf, 0).unwrap_err(); + assert!( + matches!(err, CodecError::Truncated { .. }), + "expected Truncated, got {err:?}" + ); + } + + #[test] + fn returns_overflow_error_past_max_bytes() { + // Craft MAX_BYTES+1 bytes each with continuation bit set. + let buf: Vec = (0..=MAX_BYTES).map(|_| 0x80u8).collect(); + let err = read_varint(&buf, 0).unwrap_err(); + assert!( + matches!(err, CodecError::VarintOverflow(_)), + "expected VarintOverflow, got {err:?}" + ); + } + + #[test] + fn max_bytes_constant_equals_37() { + assert_eq!(MAX_BYTES, 37); + } + + #[test] + fn bigint_uint256_max_roundtrips() { + // 32 bytes of 0xFF — the maximum uint256 value. + let uint256_max = vec![0xFFu8; 32]; + let mut buf = Vec::new(); + write_bigint_varint(&uint256_max, &mut buf); + let (decoded, bytes_consumed) = read_bigint_varint(&buf, 0).unwrap(); + assert_eq!(decoded, uint256_max, "roundtrip value mismatch"); + assert_eq!( + bytes_consumed, + buf.len(), + "bytes_consumed must equal full buffer" + ); + } + + #[test] + fn known_u64_wire_bytes() { + // Verify against TS reference values. + let cases: &[(u64, &[u8])] = &[ + (0, &[0x00]), + (1, &[0x01]), + (127, &[0x7F]), + (128, &[0x80, 0x01]), + (16384, &[0x80, 0x80, 0x01]), + (4_294_967_295, &[0xFF, 0xFF, 0xFF, 0xFF, 0x0F]), // max uint32 + ]; + for (value, expected) in cases { + let mut buf = Vec::new(); + write_varint(*value, &mut buf); + assert_eq!(&buf[..], *expected, "write_varint({value}) wire mismatch"); + let (decoded, n) = read_varint(&buf, 0).unwrap(); + assert_eq!(decoded, *value, "read_varint roundtrip failed for {value}"); + assert_eq!(n, expected.len()); + } + } + + #[cfg(not(target_arch = "wasm32"))] + proptest::proptest! { + #[test] + fn varint_roundtrips_for_any_u64(value in proptest::prelude::any::()) { + let mut buf = Vec::new(); + write_varint(value, &mut buf); + let (decoded, _) = read_varint(&buf, 0).unwrap(); + proptest::prelude::prop_assert_eq!(value, decoded); + } + } +} diff --git a/packages/codec/src/wasm.rs b/packages/codec/src/wasm.rs new file mode 100644 index 0000000..c68788b --- /dev/null +++ b/packages/codec/src/wasm.rs @@ -0,0 +1,62 @@ +//! WASM bindings — compiled only for `target_arch = "wasm32"`. +//! +//! Exports 3 functions to JS (B-v replan + Phase 2B hotfix T-P2-9b, 2026-05-20): +//! - `encodeInvoiceCanonical` — TLV canonical bytes (no Brotli) +//! - `decodeInvoiceCanonical` — Invoice from canonical bytes +//! - `receiptHash` — keccak256 of canonical bytes (ERC-3009 nonce) +//! +//! Wire encoding (Brotli + COMPRESSED_FLAG) lives in the JS shim +//! (`src/index.ts`) which wraps these and calls `brotli-wasm` as peerDep. + +#![cfg(target_arch = "wasm32")] + +use serde::Serialize; +use serde_wasm_bindgen::Serializer; +use wasm_bindgen::prelude::*; + +use crate::{Invoice, compute_content_hash, decode_invoice_canonical, encode_invoice_canonical}; + +/// BigInt-safe serializer: amounts like `u64::MAX` come back as JS BigInt, not lossy f64. +/// Required per D-B11 (BigInt boundary discipline). +fn ts_serializer() -> Serializer { + Serializer::new().serialize_large_number_types_as_bigints(true) +} + +/// Encode an Invoice to canonical TLV bytes (pre-compression, payment identity). +/// +/// The COMPRESSED_FLAG (0x80) is never set on the output — Brotli compression +/// is the caller's responsibility via the JS shim and `brotli-wasm` peerDep. +/// +/// Feed the output to `compute_content_hash()` for ERC-3009 nonce binding. +#[wasm_bindgen(js_name = encodeInvoiceCanonical)] +pub fn encode_invoice_canonical_js(invoice: JsValue) -> Result, JsError> { + let invoice: Invoice = + serde_wasm_bindgen::from_value(invoice).map_err(|e| JsError::new(&e.to_string()))?; + encode_invoice_canonical(&invoice).map_err(|e| JsError::new(&e.to_string())) +} + +/// Decode canonical TLV bytes into an Invoice object. +/// +/// Input must NOT have the COMPRESSED_FLAG set — decompress first via the JS shim. +#[wasm_bindgen(js_name = decodeInvoiceCanonical)] +pub fn decode_invoice_canonical_js(bytes: &[u8]) -> Result { + let invoice = decode_invoice_canonical(bytes).map_err(|e| JsError::new(&e.to_string()))?; + invoice + .serialize(&ts_serializer()) + .map_err(|e| JsError::new(&e.to_string())) +} + +/// keccak-256 content hash for ERC-3009 nonce binding (spec §0.2). +/// +/// Input MUST be the canonical pre-compression TLV bytes — the output of +/// `encodeInvoiceCanonical`. Returns a 32-byte Keccak-256 digest. +/// +/// Decision: receipt_hash ships in Phase 2 (plan-2c C6, Ignat 2026-05-20). +#[wasm_bindgen(js_name = receiptHash)] +pub fn receipt_hash_js(canonical_bytes: &[u8]) -> Vec { + compute_content_hash(canonical_bytes).to_vec() +} + +/// dlmalloc allocator — ~5 KB overhead, replaces the default (wee_alloc is forbidden per §3.8). +#[global_allocator] +static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; diff --git a/packages/codec/tests/bigint_boundary.rs b/packages/codec/tests/bigint_boundary.rs new file mode 100644 index 0000000..fb8a8ab --- /dev/null +++ b/packages/codec/tests/bigint_boundary.rs @@ -0,0 +1,176 @@ +// Regression test for D-B11: BigInt WASM<->JS boundary failure modes. +// Promoted from src/bin/bigint_probe.rs after spike (T-P2-0b, 2026-05-19). +// +// ACTUAL findings — D-B11 AMENDED (spec §4.8 prediction was wrong): +// - Config A (default): serde-wasm-bindgen 0.6 returns Err for ANY u64 value. +// Error: " can't be represented as a JavaScript number". Does NOT silently +// truncate — it hard-errors. Even safe values (2^53 = 9007199254740992) return Err. +// - Config B (.serialize_large_number_types_as_bigints(true)): u64 becomes JS BigInt. Exact. +// - Codec decision: amounts stored as decimal strings — safe under both configs. + +use serde::Serialize; +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_node_experimental); + +#[derive(Serialize, Clone)] +struct Probe { + u64_max: u64, + above_2_53: u64, + safe_53: u64, + string_amount: String, +} + +impl Probe { + fn new() -> Self { + Probe { + u64_max: u64::MAX, + above_2_53: 9_007_199_254_740_993_u64, // 2^53 + 1 + safe_53: 9_007_199_254_740_992_u64, // 2^53 exact + string_amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + .to_string(), // uint256::MAX decimal string + } + } +} + +// --- Config A: default serializer --- +// ACTUAL behavior (serde-wasm-bindgen 0.6): returns Err for ANY u64 value. +// Error message: " can't be represented as a JavaScript number". +// This is stricter than spec §4.8 predicted (silent truncation). D-B11 AMENDED. + +#[wasm_bindgen_test] +fn config_a_safe_u64_returns_err() { + let serializer = serde_wasm_bindgen::Serializer::new(); + let result = Probe::new().safe_53.serialize(&serializer); + // serde-wasm-bindgen 0.6 default rejects ALL u64, even values that fit in f64 mantissa + assert!( + result.is_err(), + "Config A: u64 safe_53 (2^53) must return Err — default serializer rejects all u64" + ); +} + +#[wasm_bindgen_test] +fn config_a_above_2_53_returns_err() { + let serializer = serde_wasm_bindgen::Serializer::new(); + let result = Probe::new().above_2_53.serialize(&serializer); + assert!( + result.is_err(), + "Config A: u64 above 2^53 must return Err — D-B11 failure mode (harder than truncation)" + ); +} + +#[wasm_bindgen_test] +fn config_a_u64_max_returns_err() { + let serializer = serde_wasm_bindgen::Serializer::new(); + let result = Probe::new().u64_max.serialize(&serializer); + assert!( + result.is_err(), + "Config A: u64::MAX must return Err — confirms D-B11 amended failure mode" + ); +} + +// --- Config B: BigInt-enabled serializer --- + +#[wasm_bindgen_test] +fn config_b_above_2_53_is_bigint() { + let serializer = + serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + let js_val = Probe::new().above_2_53.serialize(&serializer).unwrap(); + assert!( + js_val.is_bigint(), + "Config B: u64 above 2^53 must serialize as JS BigInt" + ); +} + +#[wasm_bindgen_test] +fn config_b_u64_max_is_exact_bigint() { + let serializer = + serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + let js_val = Probe::new().u64_max.serialize(&serializer).unwrap(); + assert!( + js_val.is_bigint(), + "Config B: u64::MAX must serialize as JS BigInt" + ); + let bigint = js_sys::BigInt::from(js_val); + let bigint_str = String::from(bigint.to_string(10).unwrap()); + assert_eq!( + bigint_str, "18446744073709551615", + "Config B: u64::MAX BigInt value must be exact" + ); +} + +#[wasm_bindgen_test] +fn config_b_safe_53_is_still_bigint() { + let serializer = + serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + let js_val = Probe::new().safe_53.serialize(&serializer).unwrap(); + assert!( + js_val.is_bigint(), + "Config B: even safe u64 becomes JS BigInt (uniform serialization)" + ); +} + +// --- String amount path (safe under both configs) --- + +#[wasm_bindgen_test] +fn string_amount_survives_config_a() { + let serializer = serde_wasm_bindgen::Serializer::new(); + let js_val = Probe::new().string_amount.serialize(&serializer).unwrap(); + let back = js_val + .as_string() + .expect("String amount must round-trip as JS string"); + assert_eq!( + back, "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "String amount (uint256::MAX) must survive Config A unchanged" + ); +} + +#[wasm_bindgen_test] +fn string_amount_survives_config_b() { + let serializer = + serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + let js_val = Probe::new().string_amount.serialize(&serializer).unwrap(); + let back = js_val + .as_string() + .expect("String amount must round-trip as JS string under Config B"); + assert_eq!( + back, "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "String amount (uint256::MAX) must survive Config B unchanged" + ); +} + +// --- Zod-equivalent: JS BigInt(v) accept/reject cases --- +// Mirrors: z.string().refine(v => { try { BigInt(v); return true; } catch { return false; } }) + +#[wasm_bindgen_test] +fn zod_refine_accepts_valid_integer_strings() { + let valid = [ + "0", + "1", + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + ]; + for s in &valid { + let result = js_sys::BigInt::new(&JsValue::from_str(s)); + assert!( + result.is_ok(), + "Zod refine: '{}' must be accepted by BigInt(v)", + s + ); + } +} + +#[wasm_bindgen_test] +fn zod_refine_rejects_invalid_strings() { + // JS BigInt() throws for scientific notation, non-numeric, decimals + let invalid = ["1e18", "abc", "1.5"]; + for s in &invalid { + let result = js_sys::BigInt::new(&JsValue::from_str(s)); + assert!( + result.is_err(), + "Zod refine: '{}' must be rejected by BigInt(v)", + s + ); + } +} diff --git a/packages/codec/tests/codec_smoke.rs b/packages/codec/tests/codec_smoke.rs new file mode 100644 index 0000000..3f81bcd --- /dev/null +++ b/packages/codec/tests/codec_smoke.rs @@ -0,0 +1,349 @@ +#![cfg(not(target_arch = "wasm32"))] + +use void_layer_codec::{ + CodecError, Invoice, InvoiceClient, InvoiceFrom, InvoiceItem, decode_invoice_canonical, + encode_invoice_canonical, +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn minimal_invoice() -> Invoice { + Invoice { + invoice_id: "INV-001".to_string(), + issued_at: 1_700_000_000, + due_at: 1_700_604_800, // +7 days + network_id: 1, // Ethereum + currency: "USDC".to_string(), + decimals: 6, + from: InvoiceFrom { + name: "Alice".to_string(), + wallet_address: "0xaabbccddee0011223344556677889900aabbccdd".to_string(), + email: None, + phone: None, + physical_address: None, + tax_id: None, + }, + client: InvoiceClient { + name: "Bob".to_string(), + wallet_address: None, + email: None, + phone: None, + physical_address: None, + tax_id: None, + }, + items: vec![InvoiceItem { + description: "Consulting".to_string(), + quantity: 1.0, + rate: "1000000".to_string(), // 1 USDC + }], + token_address: None, + notes: None, + tax: None, + discount: None, + total: "1000000".to_string(), + salt: "00112233445566778899aabbccddeeff".to_string(), + } +} + +fn full_invoice() -> Invoice { + Invoice { + invoice_id: "INV-FULL-2026".to_string(), + issued_at: 1_748_000_000, + due_at: 1_748_604_800, + network_id: 8453, // Base + currency: "ETH".to_string(), + decimals: 18, + from: InvoiceFrom { + name: "Alice Corp".to_string(), + wallet_address: "0x1111111111111111111111111111111111111111".to_string(), + email: Some("alice@example.com".to_string()), + phone: Some("+1-555-0100".to_string()), + physical_address: Some("123 Main St".to_string()), + tax_id: Some("TAX-123".to_string()), + }, + client: InvoiceClient { + name: "Bob Ltd".to_string(), + wallet_address: Some("0x2222222222222222222222222222222222222222".to_string()), + email: Some("bob@example.com".to_string()), + phone: None, + physical_address: None, + tax_id: None, + }, + items: vec![ + InvoiceItem { + description: "Development".to_string(), + quantity: 2.5, + rate: "500000000000000000".to_string(), // 0.5 ETH + }, + InvoiceItem { + description: "Consulting".to_string(), + quantity: 1.0, + rate: "200000000000000000".to_string(), // 0.2 ETH + }, + ], + token_address: None, + notes: Some("Thank you for your business".to_string()), + tax: Some("10".to_string()), + discount: Some("5".to_string()), + total: "1700000000000000000".to_string(), + salt: "aabbccddeeff00112233445566778899".to_string(), + } +} + +// --------------------------------------------------------------------------- +// Unit 2 — T-P2-8 (revised): canonical encode + decode +// --------------------------------------------------------------------------- + +#[test] +fn encodes_minimal_invoice_starts_with_magic_version() { + let invoice = minimal_invoice(); + let bytes = encode_invoice_canonical(&invoice).expect("encode failed"); + assert!(bytes.len() >= 3, "must have at least header"); + assert_eq!(bytes[0], 0x56, "magic byte must be 0x56 ('V')"); + assert_eq!(bytes[1], 0x01, "version byte must be 0x01"); +} + +#[test] +fn canonical_version_byte_has_no_compressed_flag() { + let invoice = minimal_invoice(); + let bytes = encode_invoice_canonical(&invoice).expect("encode failed"); + assert_eq!( + bytes[1] & 0x80, + 0, + "canonical bytes must NOT have COMPRESSED_FLAG (0x80) set on version byte" + ); +} + +#[test] +fn encodes_full_invoice_starts_with_magic_version() { + let invoice = full_invoice(); + let bytes = encode_invoice_canonical(&invoice).expect("encode failed"); + assert_eq!(bytes[0], 0x56); + assert_eq!(bytes[1], 0x01); +} + +#[test] +fn decodes_minimal_invoice_back() { + let original = minimal_invoice(); + let bytes = encode_invoice_canonical(&original).expect("encode failed"); + let decoded = decode_invoice_canonical(&bytes).expect("decode failed"); + assert_eq!(decoded.invoice_id, original.invoice_id); + assert_eq!(decoded.network_id, original.network_id); + assert_eq!(decoded.currency, original.currency); + assert_eq!(decoded.decimals, original.decimals); + assert_eq!(decoded.total, original.total); + assert_eq!(decoded.from.name, original.from.name); + assert_eq!(decoded.client.name, original.client.name); + assert_eq!(decoded.items.len(), original.items.len()); +} + +#[test] +fn decodes_full_invoice_back() { + let original = full_invoice(); + let bytes = encode_invoice_canonical(&original).expect("encode failed"); + let decoded = decode_invoice_canonical(&bytes).expect("decode failed"); + assert_eq!(decoded.invoice_id, original.invoice_id); + assert_eq!(decoded.from.email, original.from.email); + assert_eq!(decoded.client.email, original.client.email); + assert_eq!(decoded.notes, original.notes); + assert_eq!(decoded.tax, original.tax); + assert_eq!(decoded.discount, original.discount); + assert_eq!(decoded.items.len(), 2); +} + +#[test] +fn roundtrip_preserves_invoice_completely() { + let original = minimal_invoice(); + let bytes = encode_invoice_canonical(&original).expect("encode failed"); + let decoded = decode_invoice_canonical(&bytes).expect("decode failed"); + assert_eq!(original, decoded); +} + +#[test] +fn roundtrip_full_invoice_completely() { + let original = full_invoice(); + let bytes = encode_invoice_canonical(&original).expect("encode failed"); + let decoded = decode_invoice_canonical(&bytes).expect("decode failed"); + assert_eq!(original, decoded); +} + +#[test] +fn bad_magic_returns_error() { + let bad_bytes = vec![0x00u8, 0x01, 0x00]; // wrong magic + let err = decode_invoice_canonical(&bad_bytes).expect_err("should fail"); + assert!( + matches!(err, CodecError::BadMagic), + "expected BadMagic, got {err:?}" + ); +} + +#[test] +fn unsupported_version_returns_error() { + let bad_bytes = vec![0x56u8, 0x02, 0x00]; // version 2 not supported yet + let err = decode_invoice_canonical(&bad_bytes).expect_err("should fail"); + assert!( + matches!(err, CodecError::UnsupportedVersion(2)), + "expected UnsupportedVersion(2), got {err:?}" + ); +} + +#[test] +fn truncated_payload_returns_error() { + let bytes = vec![0x56u8, 0x01]; // only header without count + let err = decode_invoice_canonical(&bytes).expect_err("should fail"); + assert!( + matches!(err, CodecError::Truncated { .. }), + "expected Truncated, got {err:?}" + ); +} + +#[test] +fn empty_payload_returns_error() { + let err = decode_invoice_canonical(&[]).expect_err("should fail"); + assert!( + matches!(err, CodecError::BadMagic | CodecError::Truncated { .. }), + "expected BadMagic or Truncated, got {err:?}" + ); +} + +#[test] +fn encode_is_deterministic() { + let invoice = minimal_invoice(); + let bytes1 = encode_invoice_canonical(&invoice).expect("encode 1 failed"); + let bytes2 = encode_invoice_canonical(&invoice).expect("encode 2 failed"); + assert_eq!(bytes1, bytes2, "canonical encoding must be deterministic"); +} + +#[test] +fn encode_different_invoices_produce_different_bytes() { + let a = minimal_invoice(); + let mut b = minimal_invoice(); + b.total = "2000000".to_string(); + let bytes_a = encode_invoice_canonical(&a).expect("encode a failed"); + let bytes_b = encode_invoice_canonical(&b).expect("encode b failed"); + assert_ne!(bytes_a, bytes_b); +} + +#[test] +fn tlv_count_byte_matches_actual_tlv_count() { + // The 3rd byte in canonical is COUNT of TLV records. + // Minimal invoice: required fields = chain_id(2), issued_at(4), due_at(6), + // decimals(8), from_wallet(10), currency(12), items(14), from_name(16), + // client_name(18), salt(20), invoice_id(22), total(24), domain_sep(31) + // = 13 required, + optional salt already counted = 13 TLV entries minimum + let invoice = minimal_invoice(); + let bytes = encode_invoice_canonical(&invoice).expect("encode failed"); + let tlv_count = bytes[2] as usize; + assert!( + tlv_count >= 13, + "minimal invoice should have at least 13 TLV records, got {tlv_count}" + ); +} + +// --------------------------------------------------------------------------- +// Proptest: canonical encode→decode roundtrip +// --------------------------------------------------------------------------- + +use proptest::prelude::*; + +prop_compose! { + fn arb_wallet_address()( + bytes in prop::array::uniform20(any::()) + ) -> String { + use std::fmt::Write as _; + let hex = bytes.iter().fold(String::with_capacity(40), |mut s, b| { + let _ = write!(s, "{b:02x}"); + s + }); + format!("0x{hex}") + } +} + +prop_compose! { + fn arb_invoice_item()( + desc in "[a-zA-Z ]{1,20}", + qty_n in 1u32..100, + qty_d in 1u32..10, + rate in 1u64..1_000_000_000u64, + ) -> InvoiceItem { + let qty = qty_n as f64 / qty_d as f64; + // Snap to 2-decimal precision to avoid float encoding edge cases + let qty = (qty * 100.0).round() / 100.0; + InvoiceItem { + description: desc, + quantity: qty, + rate: rate.to_string(), + } + } +} + +prop_compose! { + fn arb_invoice()( + wallet in arb_wallet_address(), + issued_at in 1_600_000_000u32..1_800_000_000u32, + due_delta in 86400u32..2_592_000u32, + network_id in prop::sample::select(vec![1u32, 10, 137, 8453, 42161]), + currency in prop::sample::select(vec!["USDC", "ETH", "USDT", "DAI"]), + decimals in prop::sample::select(vec![6u8, 18]), + from_name in "[a-zA-Z ]{1,15}", + client_name in "[a-zA-Z ]{1,15}", + item in arb_invoice_item(), + total in 1u64..1_000_000_000u64, + salt_bytes in prop::array::uniform16(any::()), + ) -> Invoice { + use std::fmt::Write as _; + let salt = salt_bytes.iter().fold(String::with_capacity(32), |mut s, b| { + let _ = write!(s, "{b:02x}"); + s + }); + Invoice { + invoice_id: "INV-001".to_string(), + issued_at, + due_at: issued_at + due_delta, + network_id, + currency: currency.to_string(), + decimals, + from: InvoiceFrom { + name: from_name, + wallet_address: wallet, + email: None, + phone: None, + physical_address: None, + tax_id: None, + }, + client: InvoiceClient { + name: client_name, + wallet_address: None, + email: None, + phone: None, + physical_address: None, + tax_id: None, + }, + items: vec![item], + token_address: None, + notes: None, + tax: None, + discount: None, + total: total.to_string(), + salt, + } + } +} + +proptest! { + #[test] + fn canonical_roundtrip(inv in arb_invoice()) { + let bytes = encode_invoice_canonical(&inv).unwrap(); + let decoded = decode_invoice_canonical(&bytes).unwrap(); + prop_assert_eq!(inv, decoded); + } + + #[test] + fn canonical_encoding_is_deterministic(inv in arb_invoice()) { + let bytes1 = encode_invoice_canonical(&inv).unwrap(); + let bytes2 = encode_invoice_canonical(&inv).unwrap(); + prop_assert_eq!(bytes1, bytes2); + } +} diff --git a/packages/codec/tests/error_display.rs b/packages/codec/tests/error_display.rs new file mode 100644 index 0000000..be80576 --- /dev/null +++ b/packages/codec/tests/error_display.rs @@ -0,0 +1,58 @@ +use void_layer_codec::CodecError; + +#[test] +fn varint_overflow_displays_with_offset() { + let err = CodecError::VarintOverflow(42); + assert_eq!(err.to_string(), "varint overflow at offset 42"); +} + +#[test] +fn truncated_displays_needed_and_had() { + let err = CodecError::Truncated { needed: 10, had: 3 }; + assert_eq!(err.to_string(), "truncated payload: needed 10 bytes, had 3"); +} + +#[test] +fn unknown_extension_displays_type() { + let err = CodecError::UnknownExtension(0xAB); + assert_eq!(err.to_string(), "unknown extension TLV type 171"); +} + +#[test] +fn dictionary_mismatch_displays_expected_and_actual() { + let err = CodecError::DictionaryMismatch { + expected: 1, + actual: 2, + }; + assert_eq!(err.to_string(), "dictionary mismatch: expected 1, actual 2"); +} + +#[test] +fn signature_invalid_displays() { + let err = CodecError::SignatureInvalid; + assert_eq!(err.to_string(), "signature invalid"); +} + +#[test] +fn unsupported_version_displays() { + let err = CodecError::UnsupportedVersion(7); + assert_eq!(err.to_string(), "unsupported version 7"); +} + +#[test] +fn bad_magic_displays() { + let err = CodecError::BadMagic; + assert_eq!(err.to_string(), "bad magic bytes"); +} + +#[test] +fn checksum_mismatch_displays() { + let err = CodecError::ChecksumMismatch; + assert_eq!(err.to_string(), "checksum mismatch"); +} + +#[test] +fn compression_failed_displays_inner_message() { + let err = CodecError::CompressionFailed("buffer full".to_string()); + assert_eq!(err.to_string(), "compression failed: buffer full"); +} diff --git a/packages/codec/tests/parity.rs b/packages/codec/tests/parity.rs new file mode 100644 index 0000000..53e99ba --- /dev/null +++ b/packages/codec/tests/parity.rs @@ -0,0 +1,356 @@ +//! Golden-vector parity test — Rust surface (T-P2-13). +//! +//! Canonical only: Rust has no wire encoder (Brotli lives in the JS shim per B-v C3). +//! Wire parity is covered by tests/parity.test.ts on the TS surface. +//! +//! Reads vectors/v4-codec.json and asserts: +//! - Non-malformed: encode → canonical_hex matches; decode canonical_hex → decoded payload matches. +//! - Malformed decode-input (canonical_hex): decode → expected CodecError variant. +//! - Malformed encode-input (over-u256): encode → CodecError::InvalidAmount. + +#![cfg(not(target_arch = "wasm32"))] + +use serde::Deserialize; +use void_layer_codec::{ + CodecError, Invoice, InvoiceClient, InvoiceFrom, InvoiceItem, decode_invoice_canonical, + encode_invoice_canonical, +}; + +// --------------------------------------------------------------------------- +// Vector schema (mirrors v4-codec.json) +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +struct VectorFile { + vectors: Vec, +} + +/// A single test vector. Fields are optional because malformed vectors only +/// have a subset of them. +#[derive(Debug, Deserialize)] +struct Vector { + name: String, + /// Present on non-malformed vectors and canonical-malformed vectors. + canonical_hex: Option, + /// Present on non-malformed and encode-input malformed vectors. + decoded: Option, + /// True for non-malformed roundtrip vectors. + roundtrip: Option, + /// Classification string. + #[allow(dead_code)] + diagnostic: String, + /// Expected error variant name (present on malformed vectors). + #[allow(dead_code)] + expected_error: Option, +} + +/// JSON representation of the Invoice structure as stored in the vector file. +#[derive(Debug, Deserialize)] +struct DecodedInvoice { + invoice_id: String, + issued_at: u32, + due_at: u32, + network_id: u32, + currency: String, + decimals: u8, + from: DecodedFrom, + client: DecodedClient, + items: Vec, + #[serde(default)] + token_address: Option, + #[serde(default)] + notes: Option, + #[serde(default)] + tax: Option, + #[serde(default)] + discount: Option, + total: String, + salt: String, +} + +#[derive(Debug, Deserialize)] +struct DecodedFrom { + name: String, + wallet_address: String, + #[serde(default)] + email: Option, + #[serde(default)] + phone: Option, + #[serde(default)] + physical_address: Option, + #[serde(default)] + tax_id: Option, +} + +#[derive(Debug, Deserialize)] +struct DecodedClient { + name: String, + #[serde(default)] + wallet_address: Option, + #[serde(default)] + email: Option, + #[serde(default)] + phone: Option, + #[serde(default)] + physical_address: Option, + #[serde(default)] + tax_id: Option, +} + +#[derive(Debug, Deserialize)] +struct DecodedItem { + description: String, + quantity: f64, + rate: String, +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn load_vectors() -> VectorFile { + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/vectors/v4-codec.json"); + let raw = std::fs::read_to_string(path).expect("vectors/v4-codec.json must exist"); + serde_json::from_str(&raw).expect("v4-codec.json must be valid JSON") +} + +fn to_invoice(d: &DecodedInvoice) -> Invoice { + Invoice { + invoice_id: d.invoice_id.clone(), + issued_at: d.issued_at, + due_at: d.due_at, + network_id: d.network_id, + currency: d.currency.clone(), + decimals: d.decimals, + from: InvoiceFrom { + name: d.from.name.clone(), + wallet_address: d.from.wallet_address.clone(), + email: d.from.email.clone(), + phone: d.from.phone.clone(), + physical_address: d.from.physical_address.clone(), + tax_id: d.from.tax_id.clone(), + }, + client: InvoiceClient { + name: d.client.name.clone(), + wallet_address: d.client.wallet_address.clone(), + email: d.client.email.clone(), + phone: d.client.phone.clone(), + physical_address: d.client.physical_address.clone(), + tax_id: d.client.tax_id.clone(), + }, + items: d + .items + .iter() + .map(|i| InvoiceItem { + description: i.description.clone(), + quantity: i.quantity, + rate: i.rate.clone(), + }) + .collect(), + token_address: d.token_address.clone(), + notes: d.notes.clone(), + tax: d.tax.clone(), + discount: d.discount.clone(), + total: d.total.clone(), + salt: d.salt.clone(), + } +} + +fn from_hex(hex: &str) -> Vec { + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn to_hex(bytes: &[u8]) -> String { + use std::fmt::Write as _; + bytes + .iter() + .fold(String::with_capacity(bytes.len() * 2), |mut s, b| { + let _ = write!(s, "{b:02x}"); + s + }) +} + +// --------------------------------------------------------------------------- +// Non-malformed vectors — canonical encode + decode (both directions) +// --------------------------------------------------------------------------- + +#[test] +fn parity_canonical_encode_all_non_malformed() { + let file = load_vectors(); + let mut failures: Vec = Vec::new(); + + for v in &file.vectors { + if v.roundtrip != Some(true) { + continue; + } + let decoded = v + .decoded + .as_ref() + .expect("non-malformed vector has decoded"); + let canonical_hex = v + .canonical_hex + .as_deref() + .expect("non-malformed vector has canonical_hex"); + + let invoice = to_invoice(decoded); + match encode_invoice_canonical(&invoice) { + Ok(bytes) => { + let actual = to_hex(&bytes); + if actual != canonical_hex { + failures.push(format!( + "ENCODE MISMATCH vector={}\n expected: {}\n actual: {}", + v.name, canonical_hex, actual + )); + } + } + Err(e) => { + failures.push(format!("ENCODE ERROR vector={}: {e:?}", v.name)); + } + } + } + + assert!( + failures.is_empty(), + "Canonical encode parity failures:\n{}", + failures.join("\n\n") + ); +} + +#[test] +fn parity_canonical_decode_all_non_malformed() { + let file = load_vectors(); + let mut failures: Vec = Vec::new(); + + for v in &file.vectors { + if v.roundtrip != Some(true) { + continue; + } + let expected_decoded = v + .decoded + .as_ref() + .expect("non-malformed vector has decoded"); + let canonical_hex = v + .canonical_hex + .as_deref() + .expect("non-malformed vector has canonical_hex"); + + let bytes = from_hex(canonical_hex); + match decode_invoice_canonical(&bytes) { + Ok(actual) => { + let expected = to_invoice(expected_decoded); + if actual != expected { + failures.push(format!( + "DECODE MISMATCH vector={}\n expected: {expected:?}\n actual: {actual:?}", + v.name + )); + } + } + Err(e) => { + failures.push(format!("DECODE ERROR vector={}: {e:?}", v.name)); + } + } + } + + assert!( + failures.is_empty(), + "Canonical decode parity failures:\n{}", + failures.join("\n\n") + ); +} + +// --------------------------------------------------------------------------- +// Malformed decode-input vectors — canonical_hex → expected CodecError variant +// --------------------------------------------------------------------------- + +#[test] +fn parity_malformed_varint_overflow() { + let file = load_vectors(); + let v = file + .vectors + .iter() + .find(|v| v.name == "malformed-varint-overflow") + .expect("vector must exist"); + + let bytes = from_hex(v.canonical_hex.as_deref().expect("has canonical_hex")); + let err = decode_invoice_canonical(&bytes).expect_err("must fail"); + assert!( + matches!(err, CodecError::VarintOverflow(_)), + "expected VarintOverflow, got {err:?}" + ); +} + +#[test] +fn parity_malformed_checksum_mismatch() { + let file = load_vectors(); + let v = file + .vectors + .iter() + .find(|v| v.name == "malformed-checksum-mismatch") + .expect("vector must exist"); + + let bytes = from_hex(v.canonical_hex.as_deref().expect("has canonical_hex")); + let err = decode_invoice_canonical(&bytes).expect_err("must fail"); + assert!( + matches!(err, CodecError::ChecksumMismatch), + "expected ChecksumMismatch, got {err:?}" + ); +} + +#[test] +fn parity_malformed_oversize() { + let file = load_vectors(); + let v = file + .vectors + .iter() + .find(|v| v.name == "malformed-oversize") + .expect("vector must exist"); + + let bytes = from_hex(v.canonical_hex.as_deref().expect("has canonical_hex")); + let err = decode_invoice_canonical(&bytes).expect_err("must fail"); + assert!( + matches!(err, CodecError::Truncated { .. }), + "expected Truncated, got {err:?}" + ); +} + +#[test] +fn parity_malformed_bad_magic() { + let file = load_vectors(); + let v = file + .vectors + .iter() + .find(|v| v.name == "malformed-bad-magic") + .expect("vector must exist"); + + let bytes = from_hex(v.canonical_hex.as_deref().expect("has canonical_hex")); + let err = decode_invoice_canonical(&bytes).expect_err("must fail"); + assert!( + matches!(err, CodecError::BadMagic), + "expected BadMagic, got {err:?}" + ); +} + +// --------------------------------------------------------------------------- +// Malformed encode-input vector — bigint-amount-over-u256 → InvalidAmount +// --------------------------------------------------------------------------- + +#[test] +fn parity_malformed_encode_input_over_u256() { + let file = load_vectors(); + let v = file + .vectors + .iter() + .find(|v| v.name == "bigint-amount-over-u256") + .expect("vector must exist"); + + let decoded = v.decoded.as_ref().expect("encode-input vector has decoded"); + let invoice = to_invoice(decoded); + let err = encode_invoice_canonical(&invoice).expect_err("must fail"); + assert!( + matches!(err, CodecError::InvalidAmount(_)), + "expected InvalidAmount, got {err:?}" + ); +} diff --git a/packages/codec/tests/parity.test.ts b/packages/codec/tests/parity.test.ts new file mode 100644 index 0000000..9d094ad --- /dev/null +++ b/packages/codec/tests/parity.test.ts @@ -0,0 +1,188 @@ +/** + * Golden-vector parity test — TS/JS surface (T-P2-13). + * + * Proves both directions × both forms (canonical + wire) conform bit-exact + * to the frozen vectors in vectors/v4-codec.json. + * + * Non-malformed vectors: canonical (sync) + wire (async) encode and decode. + * Malformed decode-input: assert the thrown error contains a known substring + * that identifies the CodecError variant. The WASM layer surfaces errors as + * JS Error objects whose message matches the Rust #[error("...")] format string + * (e.g. "bad magic bytes" for BadMagic). The brotli-wasm node entry throws a raw + * string for decompression failures. Both paths are handled via ERROR_SUBSTRINGS. + * Malformed encode-input (bigint-amount-over-u256): assert InvalidAmount on encode. + */ + +import { describe, it, expect } from 'vitest' +import { + encodeInvoiceCanonical, + decodeInvoiceCanonical, +} from '../pkg/void_layer_codec.js' +import { encodeInvoiceWire, decodeInvoiceWire } from '../src/index.js' +import vectors from '../vectors/v4-codec.json' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function toHex(bytes: Uint8Array): string { + return Buffer.from(bytes).toString('hex') +} + +function fromHex(hex: string): Uint8Array { + return new Uint8Array(Buffer.from(hex, 'hex')) +} + +/** + * Maps CodecError variant names (as stored in expected_error) to a unique + * substring of the actual thrown message. The WASM layer formats errors from + * Rust's #[error("...")] strings; brotli-wasm throws a raw string for wire + * decompression failures. + * + * These substrings are stable: they are part of the codec's public error + * contract and changing them would be a breaking change. + */ +const ERROR_SUBSTRINGS: Record = { + BadMagic: 'bad magic', + VarintOverflow: 'varint overflow', + Truncated: 'truncated payload', + ChecksumMismatch: 'checksum mismatch', + CompressionFailed: 'Brotli decompress failed', + InvalidAmount: 'invalid amount', + UnsupportedVersion: 'unsupported version', + DictionaryMismatch: 'dictionary mismatch', + UnknownExtension: 'unknown extension', + SignatureInvalid: 'signature invalid', +} + +function errorSubstring(expectedError: string): string { + const sub = ERROR_SUBSTRINGS[expectedError] + if (!sub) throw new Error(`No error substring mapping for: ${expectedError}`) + return sub +} + +type AnyVector = (typeof vectors.vectors)[number] + +// Non-malformed vectors have roundtrip:true, canonical_hex, wire_hex, and decoded. +// This type guard narrows the union so those fields are known to be string/non-null. +function isNonMalformed( + v: AnyVector, +): v is AnyVector & { + canonical_hex: string + wire_hex: string + decoded: NonNullable +} { + return ( + 'roundtrip' in v && + (v as { roundtrip?: unknown }).roundtrip === true && + typeof (v as { canonical_hex?: unknown }).canonical_hex === 'string' && + typeof (v as { wire_hex?: unknown }).wire_hex === 'string' && + (v as { decoded?: unknown }).decoded != null + ) +} + +const nonMalformed = vectors.vectors.filter(isNonMalformed) + +// --------------------------------------------------------------------------- +// Non-malformed vectors — canonical (sync) + wire (async) both directions +// --------------------------------------------------------------------------- + +describe('golden-vector parity: canonical (sync)', () => { + for (const v of nonMalformed) { + it(`encode:canonical:${v.name}`, () => { + const encoded = encodeInvoiceCanonical(v.decoded) + expect(toHex(encoded)).toBe(v.canonical_hex) + }) + + it(`decode:canonical:${v.name}`, () => { + const decoded = decodeInvoiceCanonical(fromHex(v.canonical_hex)) + expect(decoded).toEqual(v.decoded) + }) + } +}) + +describe('golden-vector parity: wire (async)', () => { + for (const v of nonMalformed) { + it(`encode:wire:${v.name}`, async () => { + const encoded = await encodeInvoiceWire(v.decoded) + expect(toHex(encoded)).toBe(v.wire_hex) + }) + + it(`decode:wire:${v.name}`, async () => { + const decoded = await decodeInvoiceWire(fromHex(v.wire_hex)) + expect(decoded).toEqual(v.decoded) + }) + } +}) + +// --------------------------------------------------------------------------- +// Malformed decode-input vectors — expect error containing known substring +// --------------------------------------------------------------------------- + +describe('golden-vector parity: malformed decode-input', () => { + // Vectors with diagnostic "malformed:canonical" — decode via canonical + const malformedCanonical = vectors.vectors.filter( + (v): v is AnyVector & { canonical_hex: string; expected_error: string } => + v.diagnostic === 'malformed:canonical' && + typeof (v as { canonical_hex?: unknown }).canonical_hex === 'string' && + typeof (v as { expected_error?: unknown }).expected_error === 'string', + ) + + for (const v of malformedCanonical) { + const sub = errorSubstring(v.expected_error) + it(`malformed:canonical:${v.name} throws containing "${sub}"`, () => { + expect(() => decodeInvoiceCanonical(fromHex(v.canonical_hex))).toThrow(sub) + }) + } + + // Vectors with diagnostic "malformed:wire" — decode via wire. + // brotli-wasm node entry throws a raw string on decompress failure, + // so we catch manually and assert on String(thrown). + const malformedWire = vectors.vectors.filter( + (v): v is AnyVector & { wire_hex: string; expected_error: string } => + v.diagnostic === 'malformed:wire' && + typeof (v as { wire_hex?: unknown }).wire_hex === 'string' && + typeof (v as { expected_error?: unknown }).expected_error === 'string', + ) + + for (const v of malformedWire) { + const sub = errorSubstring(v.expected_error) + it(`malformed:wire:${v.name} throws containing "${sub}"`, async () => { + let thrown: unknown + try { + await decodeInvoiceWire(fromHex(v.wire_hex)) + } catch (e) { + thrown = e + } + expect(thrown).toBeDefined() + // brotli-wasm node entry throws a raw string, not an Error object. + // String(thrown) works for both Error.message and raw string throws. + expect(String(thrown)).toContain(sub) + }) + } +}) + +// --------------------------------------------------------------------------- +// Malformed encode-input vector — bigint-amount-over-u256 → InvalidAmount +// --------------------------------------------------------------------------- + +describe('golden-vector parity: malformed encode-input', () => { + const encodeInputMalformed = vectors.vectors.filter( + ( + v, + ): v is AnyVector & { + decoded: NonNullable + expected_error: string + } => + v.diagnostic === 'malformed:encode-input' && + (v as { decoded?: unknown }).decoded != null && + typeof (v as { expected_error?: unknown }).expected_error === 'string', + ) + + for (const v of encodeInputMalformed) { + const sub = errorSubstring(v.expected_error) + it(`malformed:encode-input:${v.name} throws containing "${sub}"`, () => { + expect(() => encodeInvoiceCanonical(v.decoded)).toThrow(sub) + }) + } +}) diff --git a/packages/codec/tests/wasm_boundary.rs b/packages/codec/tests/wasm_boundary.rs new file mode 100644 index 0000000..f86acd3 --- /dev/null +++ b/packages/codec/tests/wasm_boundary.rs @@ -0,0 +1,48 @@ +// WASM boundary test for receiptHash (compute_content_hash Rust implementation). +// Task T-P2-9b-fix — calls Rust directly, no /pkg/ re-import (2026-05-20). +// +// Tests: +// - Determinism: same input → identical digest on two calls +// - Distinctness: distinct inputs → distinct digests (guards constant-return regression) +// - 32-byte length (explicit assertion for documentation) + +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_node_experimental); + +/// Determinism: same canonical bytes → identical digest on two independent calls. +#[wasm_bindgen_test] +fn compute_content_hash_is_deterministic() { + // Hand-crafted canonical TLV: tag=0x01, length=0x03, value=[0xAA, 0xBB, 0xCC] + let canonical: &[u8] = &[0x01, 0x03, 0xAA, 0xBB, 0xCC]; + let first = void_layer_codec::compute_content_hash(canonical); + let second = void_layer_codec::compute_content_hash(canonical); + assert_eq!( + first, second, + "compute_content_hash must be deterministic — same input must yield identical digest" + ); +} + +/// Non-empty input and empty input must produce different digests +/// (guards against a constant-return regression). +#[wasm_bindgen_test] +fn compute_content_hash_distinct_for_distinct_inputs() { + let a = void_layer_codec::compute_content_hash(&[0x01, 0x03, 0xAA, 0xBB, 0xCC]); + let b = void_layer_codec::compute_content_hash(&[]); + assert_ne!( + a, b, + "compute_content_hash of distinct inputs must differ (non-constant function)" + ); +} + +/// 32-byte digest length (compile-time [u8; 32], explicit runtime assertion for documentation). +#[wasm_bindgen_test] +fn compute_content_hash_returns_32_bytes() { + let canonical: &[u8] = &[0x01, 0x03, 0xAA, 0xBB, 0xCC]; + let digest = void_layer_codec::compute_content_hash(canonical); + assert_eq!( + digest.len(), + 32, + "compute_content_hash must return exactly 32 bytes" + ); +} diff --git a/packages/codec/tsconfig.json b/packages/codec/tsconfig.json new file mode 100644 index 0000000..7196b94 --- /dev/null +++ b/packages/codec/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "pkg", "target", "src/**/*.test.ts"] +} diff --git a/packages/codec/vectors/.gitkeep b/packages/codec/vectors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/codec/vectors/spike-corpus/01-minimal-1item-evm.json b/packages/codec/vectors/spike-corpus/01-minimal-1item-evm.json new file mode 100644 index 0000000..8e2d563 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/01-minimal-1item-evm.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.049Z", + "bytes_hex": "56010d0202000204046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e6700017d071005416c6963651203426f621410424242424242424242424242424242421607494e562d30303118027d071f200f794ce253c5b4ec8afab7e444120f52be4d0e24de9b6beb299f73dce041cb7a", + "uncompressed_length": 143, + "shape": "minimal-1item-evm" +} diff --git a/packages/codec/vectors/spike-corpus/02-medium-2items-evm-notes.json b/packages/codec/vectors/spike-corpus/02-medium-2items-evm-notes.json new file mode 100644 index 0000000..2f6d0c6 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/02-medium-2items-evm-notes.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.051Z", + "bytes_hex": "56010f0202000204046a0cfc4405324e6574203330207061796d656e74207465726d732e205468616e6b20796f7520666f7220796f757220627573696e6573732e0604809a9e01070e616c696365406578616d706c65090801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e1f02094261636b656e64200d00140f070b436f646520726576696577000501081010416c696365204465762053747564696f120941636d6520436f72701410424242424242424242424242424242421607494e562d303032180223081f20eac399f6446809da6b5dd1e6d21068ffd5f833f9accc689cf55dc04f63802fe3", + "uncompressed_length": 243, + "shape": "medium-2items-evm-notes" +} diff --git a/packages/codec/vectors/spike-corpus/03-full-3items-evm-all-fields.json b/packages/codec/vectors/spike-corpus/03-full-3items-evm-all-fields.json new file mode 100644 index 0000000..6dc8c9a --- /dev/null +++ b/packages/codec/vectors/spike-corpus/03-full-3items-evm-all-fields.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.051Z", + "bytes_hex": "56011702020001031470997970c51812dc3a010c7d01b50e0d17dc79c804046a0cfc44054c506c6561736520696e636c75646520696e766f696365206e756d62657220696e207061796d656e74207265666572656e63652e20564154207265676973746572656420627573696e6573732e0604809a9e01071362696c6c696e6740616c6963656465762e696f080106090b2b312d3535352d303130300a14d8da6bf26964af9d7eed9e03e53415d37aa960450b24313233204d61696e2053742c2053616e204672616e636973636f2c2043412039343130350c0200010d0861704061636d65090e450314536d61727420636f6e7472616374206175646974000103090a46726f6e74656e64200d00100f0717546563686e6963616c20646f63756d656e746174696f6e000801080f0b2b312d3535352d303230301014416c696365204465762053747564696f204c7464112034353620436f7270204176652c204e657720596f726b2c204e59203130303031121041636d6520436f72706f726174696f6e141042424242424242424242424242424242160c494e562d3030332d46554c4c180238081f20012dfdfb63af4956e0012ac6540b85d89564f8af9848888d72ea195508a6f648230d55532d5441582d313233343536250d55532d5441582d373839303132", + "uncompressed_length": 488, + "shape": "full-3items-evm-all-fields" +} diff --git a/packages/codec/vectors/spike-corpus/04-minimal-1item-eth-mainnet.json b/packages/codec/vectors/spike-corpus/04-minimal-1item-eth-mainnet.json new file mode 100644 index 0000000..e1d5118 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/04-minimal-1item-eth-mainnet.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.052Z", + "bytes_hex": "56010d0202000104046a0cfc440604809a9e010801120a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200040e11010b44657369676e20776f726b0001011210054361726f6c1204446176651410424242424242424242424242424242421607494e562d303034180201121f200ca60085b5cf9b8b636f7242b5a214734791fb2af6948acb7298ef4c7c57cd7c", + "uncompressed_length": 145, + "shape": "minimal-1item-eth-mainnet" +} diff --git a/packages/codec/vectors/spike-corpus/05-minimal-1item-polygon.json b/packages/codec/vectors/spike-corpus/05-minimal-1item-polygon.json new file mode 100644 index 0000000..c489fd8 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/05-minimal-1item-polygon.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.052Z", + "bytes_hex": "56010d0202000404046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e11010b4c6f676f2064657369676e00010508100345766512054672616e6b1410424242424242424242424242424242421607494e562d303035180205081f20f7d298d6bc016a518d06c2d7293877a69c862fbc663d0dc79b05683e5e867288", + "uncompressed_length": 144, + "shape": "minimal-1item-polygon" +} diff --git a/packages/codec/vectors/spike-corpus/06-minimal-1item-base.json b/packages/codec/vectors/spike-corpus/06-minimal-1item-base.json new file mode 100644 index 0000000..d75f94d --- /dev/null +++ b/packages/codec/vectors/spike-corpus/06-minimal-1item-base.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.052Z", + "bytes_hex": "56010d0202000504046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e15010f41504920696e746567726174696f6e00014b0710054772616365120548656e72791410424242424242424242424242424242421607494e562d30303618024b071f20a715560230bd758279e350b37a60400a440f3262906d6e334bfcb934ce7cf69d", + "uncompressed_length": 150, + "shape": "minimal-1item-base" +} diff --git a/packages/codec/vectors/spike-corpus/07-minimal-1item-optimism.json b/packages/codec/vectors/spike-corpus/07-minimal-1item-optimism.json new file mode 100644 index 0000000..c0d99bf --- /dev/null +++ b/packages/codec/vectors/spike-corpus/07-minimal-1item-optimism.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.053Z", + "bytes_hex": "56010d0202000304046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e0d0107427567206669780002010810044972697312044a61636b1410424242424242424242424242424242421607494e562d303037180202081f2012974ee7c733e3dd45e1ca5881548a302d71d99418d3e9f768a20eeda2d28e8e", + "uncompressed_length": 140, + "shape": "minimal-1item-optimism" +} diff --git a/packages/codec/vectors/spike-corpus/08-medium-2items-usdc-arb.json b/packages/codec/vectors/spike-corpus/08-medium-2items-usdc-arb.json new file mode 100644 index 0000000..7958767 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/08-medium-2items-usdc-arb.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.053Z", + "bytes_hex": "56010d0202000204046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e2702104465466920696e746567726174696f6e000a02080c54657374696e67202620514100050507100f4b61726c20426c6f636b636861696e120d4c756e612050726f746f636f6c1410424242424242424242424242424242421607494e562d3030381803e101071f20f9cbfcd6954390d555cfbc26df4a7dbf8e4fc6f32920023d224a756ce46a8fa2", + "uncompressed_length": 187, + "shape": "medium-2items-usdc-arb" +} diff --git a/packages/codec/vectors/spike-corpus/09-medium-2items-no-notes.json b/packages/codec/vectors/spike-corpus/09-medium-2items-no-notes.json new file mode 100644 index 0000000..efeefa4 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/09-medium-2items-no-notes.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.053Z", + "bytes_hex": "56010d0202000204046a0cfc440604809a9e010801120a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200030e24020c55492f55582064657369676e000c01140d44657369676e2073797374656d00060114100a4d69612053747564696f12094e6f766120436f72701410424242424242424242424242424242421607494e562d303039180212141f206bf3cae9e22b7fd4c4bf6f0f1868a3e3d6ad1b97e075a70f85b5b467275c0249", + "uncompressed_length": 174, + "shape": "medium-2items-no-notes" +} diff --git a/packages/codec/vectors/spike-corpus/10-full-3items-client-wallet.json b/packages/codec/vectors/spike-corpus/10-full-3items-client-wallet.json new file mode 100644 index 0000000..2b9eb37 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/10-full-3items-client-wallet.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.054Z", + "bytes_hex": "56011002020001031470997970c51812dc3a010c7d01b50e0d17dc79c804046a0cfc440604809a9e01070c6f73636172406465762e696f0801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010d0e70616d4066696e616e63652e696f0e43031341726368697465637475726520726576696577000102090e496d706c656d656e746174696f6e00140108124465706c6f796d656e7420737570706f72740005010810094f7363617220446576120b50616d2046696e616e63651410424242424242424242424242424242421607494e562d30313018022d081f20ffb07298ccafb1253da7e3a33f8f6361c395849cd4927a2b02269ec8a7383b69", + "uncompressed_length": 258, + "shape": "full-3items-client-wallet" +} diff --git a/packages/codec/vectors/spike-corpus/11-full-3items-tax-discount.json b/packages/codec/vectors/spike-corpus/11-full-3items-tax-discount.json new file mode 100644 index 0000000..90e36ae --- /dev/null +++ b/packages/codec/vectors/spike-corpus/11-full-3items-tax-discount.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.054Z", + "bytes_hex": "56010f0202000204046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e37030a5374726174656779200e000102090f4d61726b657420726573656172636800010f080e5265706f72742077726974696e6700010508100c5175696e6e204167656e6379120f526f737320496e6475737472696573130231301410424242424242424242424242424242421501351607494e562d3031311803d803071f2001098ae6f851df895dfa5fb74fae5fa7cd39233623f2bcf5fc6267ba9815bc75", + "uncompressed_length": 209, + "shape": "full-3items-tax-discount" +} diff --git a/packages/codec/vectors/spike-corpus/12-medium-2items-long-descriptions.json b/packages/codec/vectors/spike-corpus/12-medium-2items-long-descriptions.json new file mode 100644 index 0000000..b815f90 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/12-medium-2items-long-descriptions.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.055Z", + "bytes_hex": "56010e0202000204046a0cfc440549457874656e64656420656e676167656d656e7420666f7220513220323032362070726f64756374200d20737072696e7420636f766572696e6720616c6c206d696c6573746f6e65732e0604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010ecd01025e46756c6c2d737461636b20776562206170706c69636174696f6e200d20696e636c7564696e67206261636b656e64204150492c20646174616261736520736368656d612064657369676e2c20616e642052656163742066726f6e74656e64000104096443492f434420706970656c696e652073657475702c20446f636b657220636f6e7461696e6572697a6174696f6e2c20415753206465706c6f796d656e742c206d6f6e69746f72696e6720616e6420616c657274696e6720636f6e66696775726174696f6e00010209100f53616d20456e67696e656572696e67120d546572726120537461727475701410424242424242424242424242424242421607494e562d303132180206091f20b3656a2f0df7d584a608bf2ae546ecab637515771ef33e3ca81f4cea47c7d4ca", + "uncompressed_length": 428, + "shape": "medium-2items-long-descriptions" +} diff --git a/packages/codec/vectors/spike-corpus/13-minimal-1item-raw-currency.json b/packages/codec/vectors/spike-corpus/13-minimal-1item-raw-currency.json new file mode 100644 index 0000000..bfb76a7 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/13-minimal-1item-raw-currency.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.055Z", + "bytes_hex": "56010d0202000204046a0cfc440604809a9e010801080a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200080e1b0115426974636f696e20637573746f647920736574757000010106100b556d6120426974636f696e120b566963746f722046756e641410424242424242424242424242424242421607494e562d303133180201061f20213d217a2f8d626d544e6930c2b153bbb98682f05ceeae5f63bdbe81e5351b80", + "uncompressed_length": 168, + "shape": "minimal-1item-raw-currency" +} diff --git a/packages/codec/vectors/spike-corpus/14-full-3items-all-optional-text.json b/packages/codec/vectors/spike-corpus/14-full-3items-all-optional-text.json new file mode 100644 index 0000000..e5624ee --- /dev/null +++ b/packages/codec/vectors/spike-corpus/14-full-3items-all-optional-text.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.056Z", + "bytes_hex": "56011702020001031470997970c51812dc3a010c7d01b50e0d17dc79c804046a0cfc44054707206475652077697468696e20333020646179732e204c6174652066656573206f6620312e352520706572206d6f6e7468206170706c792061667465722064756520646174652e0604809a9e01071677656e64794074656368736f6c7574696f6e732e696f08010609102b34342d32302d373934362d303935380a14d8da6bf26964af9d7eed9e03e53415d37aa960450b22313020446f776e696e672053742c204c6f6e646f6e2c20554b2053573141203241410c0200010d1378617669657240656e746572707269736573090e61031b456e746572707269736520736f667477617265206c6963656e7365000105091b496d706c656d656e746174696f6e2026206f6e626f617264696e67000103091b4669727374207965617220737570706f727420636f6e747261637400010f080f0f2b312d3231322d3535352d30313530101457656e6479205465636820536f6c7574696f6e7311283120576f726c642054726164652043656e7465722c204e657720596f726b2c204e59203130303037121258617669657220456e746572707269736573141042424242424242424242424242424242161b494e562d3031342d4c4f4e472d49442d464f522d54455354494e4718025f081f20c127b39c6ec0d0e01389475941b8fe4bb15d899cbcb6a54fcfa6a98726be6d12231047422d5641542d313233343536373839251155532d45494e2d31322d33343536373839", + "uncompressed_length": 564, + "shape": "full-3items-all-optional-text" +} diff --git a/packages/codec/vectors/spike-corpus/15-minimal-1item-small-amount.json b/packages/codec/vectors/spike-corpus/15-minimal-1item-small-amount.json new file mode 100644 index 0000000..0cf01fa --- /dev/null +++ b/packages/codec/vectors/spike-corpus/15-minimal-1item-small-amount.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.056Z", + "bytes_hex": "56010d0202000404046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e11010b5472616e736c6174696f6e0001050610045961726112035a6f651410424242424242424242424242424242421607494e562d303135180205061f208c69c5439a47b5b14ad360ac45e52312b8070cc81c3a57b5226de2efd53d39e1", + "uncompressed_length": 143, + "shape": "minimal-1item-small-amount" +} diff --git a/packages/codec/vectors/spike-corpus/16-minimal-1item-large-amount.json b/packages/codec/vectors/spike-corpus/16-minimal-1item-large-amount.json new file mode 100644 index 0000000..7be555b --- /dev/null +++ b/packages/codec/vectors/spike-corpus/16-minimal-1item-large-amount.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.056Z", + "bytes_hex": "56010d0202000104046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e23011d50726f746f636f6c206163717569736974696f6e2061647669736f72790001050b100d41746c6173204361706974616c12094e657875732044414f1410424242424242424242424242424242421607494e562d3031361802050b1f2032948fb4f12ef591afb3160a281668fca72e6863bbd46c7c7477661752f91b3f", + "uncompressed_length": 176, + "shape": "minimal-1item-large-amount" +} diff --git a/packages/codec/vectors/spike-corpus/17-medium-2items-fractional-qty.json b/packages/codec/vectors/spike-corpus/17-medium-2items-fractional-qty.json new file mode 100644 index 0000000..51f2442 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/17-medium-2items-fractional-qty.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.056Z", + "bytes_hex": "56010d0202000504046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e3302154272616e64206964656e746974792064657369676e010f040813536f6369616c206d656469612061737365747301190707100c426c616b652044657369676e120a4379616e204d656469611410424242424242424242424242424242421607494e562d3031371803eb06061f20de8c9deac6b2b42253aec96e31dd4eb90e6dfb8dfff71ec773508c0caa8cc0d2", + "uncompressed_length": 193, + "shape": "medium-2items-fractional-qty" +} diff --git a/packages/codec/vectors/spike-corpus/18-full-3items-eip712-heavy.json b/packages/codec/vectors/spike-corpus/18-full-3items-eip712-heavy.json new file mode 100644 index 0000000..b5adfcb --- /dev/null +++ b/packages/codec/vectors/spike-corpus/18-full-3items-eip712-heavy.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.057Z", + "bytes_hex": "56011102020002031470997970c51812dc3a010c7d01b50e0d17dc79c804046a0cfc4405394549502d373132207369676e656420696e766f69636520666f72206f6e2d636861696e207061796d656e7420766572696669636174696f6e2e0604809a9e010715647265774070726f746f636f6c6c6162732e78797a0801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010d15747265617375727940656d62657264616f2e78797a0e57031c50726f746f636f6c2064657369676e202620746f6b656e6f6d6963730001030910536d61727420636f6e7472616374200d000103091b536563757269747920617564697420636f6f7264696e6174696f6e00010d081012447265772050726f746f636f6c204c6162731212456d6265722044414f205472656173757279141042424242424242424242424242424242160e494e562d3031382d454950373132180249081f2008b7d6fcd855ca1e1ba65de878ecc5706eeb1fd83e55c9198e4d2cd8043c08a3", + "uncompressed_length": 376, + "shape": "full-3items-eip712-heavy" +} diff --git a/packages/codec/vectors/spike-corpus/19-medium-2items-long-invoiceid.json b/packages/codec/vectors/spike-corpus/19-medium-2items-long-invoiceid.json new file mode 100644 index 0000000..7c946f4 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/19-medium-2items-long-invoiceid.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.057Z", + "bytes_hex": "56010d0202000304046a0cfc440604809a9e010801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e43021b537072696e7420706c616e6e696e67202620657865637574696f6e000102091d526574726f7370656374697665202620646f63756d656e746174696f6e00010608100b466179652053747564696f120d47616c652056656e74757265731410424242424242424242424242424242421626494e564f4943452d323032362d51322d444556454c4f504d454e542d535052494e542d30343218021a081f2088c0aed8f0d3c3760e0c6eb2211d94dab19d90bb9b98f994a6c9c64aa12bbc39", + "uncompressed_length": 241, + "shape": "medium-2items-long-invoiceid" +} diff --git a/packages/codec/vectors/spike-corpus/20-full-3items-both-emails.json b/packages/codec/vectors/spike-corpus/20-full-3items-both-emails.json new file mode 100644 index 0000000..70c2c84 --- /dev/null +++ b/packages/codec/vectors/spike-corpus/20-full-3items-both-emails.json @@ -0,0 +1,7 @@ +{ + "source": "synthetic-via-ts-codec", + "generated_at": "2026-05-20T00:11:48.058Z", + "bytes_hex": "56011202020002031470997970c51812dc3a010c7d01b50e0d17dc79c804046a0cfc440604809a9e01070a68616e6b400e2e6465760801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010d1862696c6c696e67406976792d736f6c7574696f6e732e64650e4e03125765623320696e746567726174696f6e200e000f0f0717546563686e6963616c206475652064696c6967656e63650008010815576f726b73686f7020666163696c69746174696f6e00030208100f48616e6b20436f6e73756c74696e67121249767920536f6c7574696f6e7320476d62481410424242424242424242424242424242421607494e562d3032301803cf02071f202eb07112990e30ff082a00c86133ca452be088ab8f2ac23939394c79fb15e790231044452d5553742d313233343536373839251044452d5553742d393837363534333231", + "uncompressed_length": 327, + "shape": "full-3items-both-emails" +} diff --git a/packages/codec/vectors/v4-codec.json b/packages/codec/vectors/v4-codec.json new file mode 100644 index 0000000..23c7241 --- /dev/null +++ b/packages/codec/vectors/v4-codec.json @@ -0,0 +1,444 @@ +{ + "schema_version": 1, + "generated_by": "@void-layer/codec v0.0.0", + "generated_at": "2026-05-20", + "vectors": [ + { + "name": "minimal-single-tlv", + "canonical_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef1607494e562d303031180201061f20e7620cf63c7f087f05bd266fba981b1e79c3697a22fcaf710f6c2b69db868be5", + "wire_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef1607494e562d303031180201061f20e7620cf63c7f087f05bd266fba981b1e79c3697a22fcaf710f6c2b69db868be5", + "decoded": { + "invoice_id": "INV-001", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000000" + } + ], + "total": "1000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Smallest valid invoice — all required fields, one item, no optional fields. wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "chain-ethereum", + "canonical_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160b494e562d434841494e2d31180201061f20fa54a336521772037a5a0127ff47deb98280f10d36be6941b217be5a84641a76", + "wire_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160b494e562d434841494e2d31180201061f20fa54a336521772037a5a0127ff47deb98280f10d36be6941b217be5a84641a76", + "decoded": { + "invoice_id": "INV-CHAIN-1", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000000" + } + ], + "total": "1000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Chain selector: ethereum (network_id=1). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "chain-base", + "canonical_hex": "56010d0202000504046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160e494e562d434841494e2d38343533180201061f20402eea0969408f0ab8b2ca2896d6010e3f185d4de79250c8db0b54e578cd2b2a", + "wire_hex": "56010d0202000504046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160e494e562d434841494e2d38343533180201061f20402eea0969408f0ab8b2ca2896d6010e3f185d4de79250c8db0b54e578cd2b2a", + "decoded": { + "invoice_id": "INV-CHAIN-8453", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 8453, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000000" + } + ], + "total": "1000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Chain selector: base (network_id=8453). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "chain-arbitrum", + "canonical_hex": "56010d0202000204046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160f494e562d434841494e2d3432313631180201061f20b13545af610e6709e7ca7e7aeb2a7656fc660281153cbfaf92d624886091d835", + "wire_hex": "56010d0202000204046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160f494e562d434841494e2d3432313631180201061f20b13545af610e6709e7ca7e7aeb2a7656fc660281153cbfaf92d624886091d835", + "decoded": { + "invoice_id": "INV-CHAIN-42161", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 42161, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000000" + } + ], + "total": "1000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Chain selector: arbitrum (network_id=42161). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "chain-optimism", + "canonical_hex": "56010d0202000304046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160c494e562d434841494e2d3130180201061f20e7953796936e016c128c25ce8860ea9ecbf3ca6b0951d237df288a72ea1b1a04", + "wire_hex": "56010d0202000304046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160c494e562d434841494e2d3130180201061f20e7953796936e016c128c25ce8860ea9ecbf3ca6b0951d237df288a72ea1b1a04", + "decoded": { + "invoice_id": "INV-CHAIN-10", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 10, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000000" + } + ], + "total": "1000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Chain selector: optimism (network_id=10). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "chain-polygon", + "canonical_hex": "56010d0202000404046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160d494e562d434841494e2d313337180201061f20f7b0ae2ea635ca1810c68c0e432202688c0080e483db39c05a8c92b47f8e285d", + "wire_hex": "56010d0202000404046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e10010a436f6e73756c74696e67000101061005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160d494e562d434841494e2d313337180201061f20f7b0ae2ea635ca1810c68c0e432202688c0080e483db39c05a8c92b47f8e285d", + "decoded": { + "invoice_id": "INV-CHAIN-137", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 137, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000000" + } + ], + "total": "1000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Chain selector: polygon (network_id=137). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "bigint-amount-zero", + "canonical_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e12010c5a65726f207061796d656e74000100001005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160f494e562d424947494e542d5a45524f180200001f2028eb733c3fee47d262e9cbf37f683c1db35d26618dcca6c5333c181a8490780a", + "wire_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e12010c5a65726f207061796d656e74000100001005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160f494e562d424947494e542d5a45524f180200001f2028eb733c3fee47d262e9cbf37f683c1db35d26618dcca6c5333c181a8490780a", + "decoded": { + "invoice_id": "INV-BIGINT-ZERO", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Zero payment", + "quantity": 1, + "rate": "0" + } + ], + "total": "0", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "BigInt edge: total = 0 (LEB128 single 0x00 byte). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "bigint-amount-one", + "canonical_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e15010f4f6e652061746f6d696320756e6974000101001005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160e494e562d424947494e542d4f4e45180201001f2041c80d97247077111cdc112ff9973a4f9f7d985863b9f04e6470f76d4db9277f", + "wire_hex": "56010d0202000104046553f100060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e15010f4f6e652061746f6d696320756e6974000101001005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160e494e562d424947494e542d4f4e45180201001f2041c80d97247077111cdc112ff9973a4f9f7d985863b9f04e6470f76d4db9277f", + "decoded": { + "invoice_id": "INV-BIGINT-ONE", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "One atomic unit", + "quantity": 1, + "rate": "1" + } + ], + "total": "1", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "BigInt edge: total = 1 (smallest nonzero, no trailing zeros). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "bigint-amount-uint256-max", + "canonical_hex": "56010d0202000104046553f100060380a3050801120a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200040e3d01134d61782075696e74323536207061796d656e740001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0f001005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef1612494e562d424947494e542d553235364d41581826ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0f001f2090797da436f09e0d536666e0e918c25f6945347b1fcd926595ab2dcad94bc159", + "wire_hex": "56811be700708fd456f78b626c609c0e50dbd21adc2035d021ee8a4f62688c4a4674284fcfac74d88013b478935df60596c121b7db638c39ad8a058100400824e1f905c0c0e3052802484251ae6f737f99f8cefcf013f8434c3debae8575181000829301c9b6489bd5c814eb42897466b800e0bfe00104a8ea94ba90c0ea52944278dcdafd46692493ddcf519b0c79e0e5f88412a94d1564b07b80c99a74068bd21f584f32f93ce33094d1897bccd3e9c4dc06e7e8c67210", + "decoded": { + "invoice_id": "INV-BIGINT-U256MAX", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "ETH", + "decimals": 18, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Max uint256 payment", + "quantity": 1, + "rate": "115792089237316195423570985008687907853269984665640564039457584007913129639935" + } + ], + "total": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "BigInt edge: total = U256::MAX (115792089237316195423570985008687907853269984665640564039457584007913129639935) — largest encodable value after U256 widening. wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "bigint-amount-over-u256", + "decoded": { + "invoice_id": "INV-BIGINT-OVER-U256", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "ETH", + "decimals": 18, + "from": { + "name": "Alice", + "wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Over U256 payment", + "quantity": 1, + "rate": "115792089237316195423570985008687907853269984665640564039457584007913129639936" + } + ], + "total": "115792089237316195423570985008687907853269984665640564039457584007913129639936", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "diagnostic": "malformed:encode-input", + "expected_error": "InvalidAmount" + }, + { + "name": "malformed-checksum-mismatch", + "canonical_hex": "56010118268080808080808080808080808080808080808080808080808080808080808080808080808080", + "diagnostic": "malformed:canonical", + "expected_error": "ChecksumMismatch" + }, + { + "name": "malformed-varint-overflow", + "canonical_hex": "5601011880808080808080808080808080808080808080808080808080808080808080808080808080", + "diagnostic": "malformed:canonical", + "expected_error": "VarintOverflow" + }, + { + "name": "extension-magic-dust", + "canonical_hex": "56010e0202000104046553f10005314d616769632064757374206170706c6965643a202b302e30303030343220666f7220756e69717565206d61746368696e67060380a3050801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e12010a436f6e73756c74696e670001ea843d001005416c6963651203426f621410deadbeefdeadbeefdeadbeefdeadbeef160c494e562d4558542d445553541804ea843d001f2042c92a6131ebc43be9ebc980c4bb881acb115cc011a9820caf74db4a8a84712b", + "wire_hex": "56811bc7000064625c3ea82c3458331319fa9ce6eb64949aac9303e6ff3b480905a245d61642ba4376edd9bd234a74249a7120100008819445fe0050c50087cdce0f972981482e9533d5caac742d2bb6bbaafa0a18b867168a00c25094ebdb9affaad295f1aeaf09f0939a7ad6365768c38000401c0988dae406cf1b00f83ea00310a0a6daaa923212d8dc504c213c2e6dfca0a1615cfeb8c4968c4aae6e0e06440b7c81f95858a4f8d8d3bc7d1cf7ecad8eb04e89d92de25c1f66a5f5ce3d36d02402", + "decoded": { + "invoice_id": "INV-EXT-DUST", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Consulting", + "quantity": 1, + "rate": "1000042" + } + ], + "notes": "Magic dust applied: +0.000042 for unique matching", + "total": "1000042", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Extension: magic-dust (micro-amount uniquifier in total + notes field). wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "extension-og-param", + "canonical_hex": "56011002020001031470997970c51812dc3a010c7d01b50e0d17dc79c804046553f1000519506c65617365207061792077697468696e2033302064617973060380a305070c616c696365406465762e696f0801060a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200010e11010b44657369676e20776f726b000105061010416c696365204465762053747564696f120941636d6520436f72701410deadbeefdeadbeefdeadbeefdeadbeef160a494e562d4558542d4f47180205061f20b45936d1745dfc433d14c3b650505d67eb70799bce4371c399e1aec078c09c81", + "wire_hex": "56811bdf00788c53acdf37b0ce0630ed4299a41750a290e4282440090a7e9d1ce801f9bbd077a14028e1b62c929c728b43cea220d6f80cdf20412020309456d5d35cb5c3a2dca9c0b876f03201cfbc6bde87c1f2c37f20383b495576f34d592c00cc0902da350647e2b2cb8a73f30d79f90dbce24a141881a15ddd94fe17e7cd0e747c0d42df25f4d396f12c2b0e020213c860ec786c2a080c4790484601c0a237f28f8236e696e703e6ca9a2a1ae9617af5070d03e3f4c5b8d64484ebb7b3201ce0004b49d9715dba594bdb5a0904d20b3faa9afb0eccd55b3dcf33eb4debfddd", + "decoded": { + "invoice_id": "INV-EXT-OG", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 1, + "currency": "USDC", + "decimals": 6, + "from": { + "name": "Alice Dev Studio", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", + "email": "alice@dev.io" + }, + "client": { + "name": "Acme Corp", + "wallet_address": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8" + }, + "items": [ + { + "description": "Design work", + "quantity": 1, + "rate": "5000000" + } + ], + "notes": "Please pay within 30 days", + "total": "5000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Extension: OG-param fields (from.email, client.wallet_address, notes) for social preview. wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "extension-sub-invoice-chain", + "canonical_hex": "56010f0202000204046553f100060380a3050801120a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200040e13010d43726f73732d636861696e200e000105111005416c6963651203426f62130231301410deadbeefdeadbeefdeadbeefdeadbeef1501351610494e562d4558542d535542434841494e180205111f203a4af626efeeeb80f837d8d51c38cbe8d942f329caee547628e84effccee6a26", + "wire_hex": "56010f0202000204046553f100060380a3050801120a14d8da6bf26964af9d7eed9e03e53415d37aa960450c0200040e13010d43726f73732d636861696e200e000105111005416c6963651203426f62130231301410deadbeefdeadbeefdeadbeefdeadbeef1501351610494e562d4558542d535542434841494e180205111f203a4af626efeeeb80f837d8d51c38cbe8d942f329caee547628e84effccee6a26", + "decoded": { + "invoice_id": "INV-EXT-SUBCHAIN", + "issued_at": 1700000000, + "due_at": 1700086400, + "network_id": 42161, + "currency": "ETH", + "decimals": 18, + "from": { + "name": "Alice", + "wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + }, + "client": { + "name": "Bob" + }, + "items": [ + { + "description": "Cross-chain consulting", + "quantity": 1, + "rate": "500000000000000000" + } + ], + "tax": "10", + "discount": "5", + "total": "500000000000000000", + "salt": "deadbeefdeadbeefdeadbeefdeadbeef" + }, + "roundtrip": true, + "diagnostic": "Extension: sub-invoice chain — ETH on Arbitrum with tax and discount fields. wire_hex = Brotli-compressed wire, or == canonical_hex when Brotli expands (small payloads)" + }, + { + "name": "malformed-corrupted-brotli", + "wire_hex": "5681deadbeefcafebabe", + "diagnostic": "malformed:wire", + "expected_error": "CompressionFailed" + }, + { + "name": "malformed-oversize", + "canonical_hex": "56010118d60b00000000", + "diagnostic": "malformed:canonical", + "expected_error": "Truncated" + }, + { + "name": "malformed-bad-magic", + "canonical_hex": "ff010118020100", + "diagnostic": "malformed:canonical", + "expected_error": "BadMagic" + } + ] +} diff --git a/packages/codec/vitest.config.ts b/packages/codec/vitest.config.ts new file mode 100644 index 0000000..8814e11 --- /dev/null +++ b/packages/codec/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, configDefaults } from 'vitest/config' +import wasm from 'vite-plugin-wasm' +import topLevelAwait from 'vite-plugin-top-level-await' +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) + +export default defineConfig({ + plugins: [wasm(), topLevelAwait()], + test: { + environment: 'node', + // Exclude the generator wrapper from the default test run — regeneration + // is an explicit manual step, not something that should run on every pnpm test. + exclude: [...configDefaults.exclude, 'scripts/**'], + }, + resolve: { + alias: { + // brotli-wasm's ESM condition routes to index.web.js, which loads WASM via + // fetch() — unavailable in the vitest Node env. The bare specifier resolved + // through CJS conditions lands on index.node.js (synchronous). The + // '/index.node.js' subpath is not in the package's exports map, so it must + // be resolved as the bare specifier, not appended. + 'brotli-wasm': require.resolve('brotli-wasm'), + }, + }, +}) diff --git a/packages/networks/.npmignore b/packages/networks/.npmignore new file mode 100644 index 0000000..bab3931 --- /dev/null +++ b/packages/networks/.npmignore @@ -0,0 +1,2 @@ +src/ +tsconfig.json diff --git a/packages/networks/README.md b/packages/networks/README.md new file mode 100644 index 0000000..d88e4bf --- /dev/null +++ b/packages/networks/README.md @@ -0,0 +1,36 @@ +# @void-layer/networks + +Chain configs + token list for the `@void-layer` ecosystem. + +## Install + +```bash +pnpm add @void-layer/networks +``` + +## Usage + +```typescript +import { SUPPORTED_CHAINS, getPublicRpcUrl } from '@void-layer/networks'; + +const eth = SUPPORTED_CHAINS[1]; +// { chainId: 1, name: 'Ethereum', rpcUrls: [...], ... } + +const url = getPublicRpcUrl(1); +// 'https://eth.llamarpc.com' +``` + +## Privacy note + +**NO RPC KEYS in this package.** All URLs are public endpoints (llamarpc.com). +Server-side API keys (Alchemy, Infura, etc.) live in `voidpay.xyz` only — never shipped in client bundles. + +`SUPPORTED_TOKENS` is empty in Phase 1. Phase 2 populates from Uniswap Token List. + +## Supported chains + +Ethereum (1), Base (8453), Arbitrum One (42161), Optimism (10), Polygon (137). + +## Reference + +Design: [spec 056](https://github.com/ignromanov/voidpay-ai/tree/main/ops/specs/056-void-layer-codec-extraction) diff --git a/packages/networks/package.json b/packages/networks/package.json new file mode 100644 index 0000000..4059765 --- /dev/null +++ b/packages/networks/package.json @@ -0,0 +1,27 @@ +{ + "name": "@void-layer/networks", + "version": "0.0.0", + "description": "Chain configs + token list for @void-layer ecosystem. NO RPC keys.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } + }, + "files": ["dist/", "README.md", "LICENSE"], + "repository": { + "type": "git", + "url": "git+https://github.com/void-layer/codec.git", + "directory": "packages/networks" + }, + "license": "MIT", + "dependencies": { + "@void-layer/types": "workspace:*" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "echo 'Phase 1 stub'", + "lint": "echo 'Phase 1 stub'" + }, + "engines": { "node": ">=24" } +} diff --git a/packages/networks/src/chains.ts b/packages/networks/src/chains.ts new file mode 100644 index 0000000..91de32a --- /dev/null +++ b/packages/networks/src/chains.ts @@ -0,0 +1,39 @@ +import type { ChainId, NetworkConfig } from '@void-layer/types'; + +export const SUPPORTED_CHAINS: Record = { + 1: { + chainId: 1, + name: 'Ethereum', + rpcUrls: ['https://eth.llamarpc.com'], + blockExplorer: 'https://etherscan.io', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + }, + 8453: { + chainId: 8453, + name: 'Base', + rpcUrls: ['https://base.llamarpc.com'], + blockExplorer: 'https://basescan.org', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + }, + 42161: { + chainId: 42161, + name: 'Arbitrum One', + rpcUrls: ['https://arbitrum.llamarpc.com'], + blockExplorer: 'https://arbiscan.io', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + }, + 10: { + chainId: 10, + name: 'Optimism', + rpcUrls: ['https://optimism.llamarpc.com'], + blockExplorer: 'https://optimistic.etherscan.io', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + }, + 137: { + chainId: 137, + name: 'Polygon', + rpcUrls: ['https://polygon.llamarpc.com'], + blockExplorer: 'https://polygonscan.com', + nativeCurrency: { name: 'POL', symbol: 'POL', decimals: 18 }, + }, +}; diff --git a/packages/networks/src/index.ts b/packages/networks/src/index.ts new file mode 100644 index 0000000..d5d4008 --- /dev/null +++ b/packages/networks/src/index.ts @@ -0,0 +1,4 @@ +export { SUPPORTED_CHAINS } from './chains.js'; +export { SUPPORTED_TOKENS } from './tokens.js'; +export { getPublicRpcUrl } from './rpc.js'; +export type { TokenInfo } from './tokens.js'; diff --git a/packages/networks/src/rpc.ts b/packages/networks/src/rpc.ts new file mode 100644 index 0000000..1331074 --- /dev/null +++ b/packages/networks/src/rpc.ts @@ -0,0 +1,8 @@ +import type { ChainId } from '@void-layer/types'; +import { SUPPORTED_CHAINS } from './chains.js'; + +export function getPublicRpcUrl(chainId: ChainId): string { + const chain = SUPPORTED_CHAINS[chainId]; + if (!chain) throw new Error(`Unsupported chainId: ${chainId}`); + return chain.rpcUrls[0] ?? (() => { throw new Error(`No rpcUrl for chainId: ${chainId}`); })(); +} diff --git a/packages/networks/src/tokens.ts b/packages/networks/src/tokens.ts new file mode 100644 index 0000000..632ecc4 --- /dev/null +++ b/packages/networks/src/tokens.ts @@ -0,0 +1,12 @@ +import type { ChainId } from '@void-layer/types'; + +export interface TokenInfo { + address: string; + chainId: ChainId; + symbol: string; + decimals: number; + name: string; +} + +// Phase 2 populates from Uniswap Token List +export const SUPPORTED_TOKENS: readonly TokenInfo[] = []; diff --git a/packages/networks/tsconfig.json b/packages/networks/tsconfig.json new file mode 100644 index 0000000..7f14a4a --- /dev/null +++ b/packages/networks/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "strict": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/types/.npmignore b/packages/types/.npmignore new file mode 100644 index 0000000..bab3931 --- /dev/null +++ b/packages/types/.npmignore @@ -0,0 +1,2 @@ +src/ +tsconfig.json diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 0000000..803c678 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,35 @@ +# @void-layer/types + +Manual TypeScript types for the `@void-layer` ecosystem. Zero runtime dependencies. + +## Install + +```sh +pnpm add @void-layer/types +``` + +## Contents + +| Module | Exports | +|--------|---------| +| `network` | `ChainId`, `NetworkConfig` | +| `x402` | `PaymentProof`, `PaymentRequiredResponse` | +| `frame` | `FrameContext`, `FrameState` | + +## Usage + +```ts +import type { ChainId, NetworkConfig } from '@void-layer/types'; +import type { PaymentProof } from '@void-layer/types'; +import type { FrameContext, FrameState } from '@void-layer/types'; +``` + +## Notes + +- Types only — zero runtime code, zero `const`, zero functions +- No dependencies +- Part of the `@void-layer/codec` monorepo — see [spec 056](https://github.com/ignromanov/voidpay-ai) for design rationale + +## License + +MIT diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000..6c14307 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,24 @@ +{ + "name": "@void-layer/types", + "version": "0.0.0", + "description": "@void-layer manual TypeScript types — zero runtime deps", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } + }, + "files": ["dist/", "README.md", "LICENSE"], + "repository": { + "type": "git", + "url": "git+https://github.com/void-layer/codec.git", + "directory": "packages/types" + }, + "license": "MIT", + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "echo 'Phase 1 stub — type-level tests land Phase 2'", + "lint": "echo 'Phase 1 stub'" + }, + "engines": { "node": ">=24" } +} diff --git a/packages/types/src/frame.ts b/packages/types/src/frame.ts new file mode 100644 index 0000000..6cab629 --- /dev/null +++ b/packages/types/src/frame.ts @@ -0,0 +1,12 @@ +import type { ChainId } from './network.js'; + +export interface FrameContext { + fid: number; + username: string; + displayName: string; +} + +export interface FrameState { + invoiceId: string; + network: ChainId; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 0000000..b08032a --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,3 @@ +export type { ChainId, NetworkConfig } from './network.js'; +export type { PaymentProof, PaymentRequiredResponse } from './x402.js'; +export type { FrameContext, FrameState } from './frame.js'; diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts new file mode 100644 index 0000000..a7455e7 --- /dev/null +++ b/packages/types/src/network.ts @@ -0,0 +1,9 @@ +export type ChainId = 1 | 8453 | 42161 | 10 | 137; + +export interface NetworkConfig { + chainId: ChainId; + name: string; + rpcUrls: readonly string[]; + blockExplorer: string; + nativeCurrency: { name: string; symbol: string; decimals: number }; +} diff --git a/packages/types/src/x402.ts b/packages/types/src/x402.ts new file mode 100644 index 0000000..9561e2b --- /dev/null +++ b/packages/types/src/x402.ts @@ -0,0 +1,13 @@ +export interface PaymentProof { + version: string; + invoiceHash: string; + signature: string; + chainId: number; + expiry: number; + payer: string; +} + +export interface PaymentRequiredResponse { + invoiceUrl: string; + paymentProof?: PaymentProof; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000..7e8f270 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..a735d33 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3168 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.27.0 + version: 2.31.0(@types/node@25.9.1) + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + eslint: + specifier: ^9.39.4 + version: 9.39.4 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.59.4 + version: 8.59.4(eslint@9.39.4)(typescript@6.0.3) + + packages/codec: + devDependencies: + '@types/node': + specifier: 25.9.1 + version: 25.9.1 + '@vitest/coverage-v8': + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4(@types/node@25.9.1)) + brotli-wasm: + specifier: ^3.0.1 + version: 3.0.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite-plugin-top-level-await: + specifier: ^1.4.4 + version: 1.6.0(rollup@4.60.4)(vite@7.3.3(@types/node@25.9.1)) + vite-plugin-wasm: + specifier: ^3.4.1 + version: 3.6.0(vite@7.3.3(@types/node@25.9.1)) + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@25.9.1) + + packages/networks: + dependencies: + '@void-layer/types': + specifier: workspace:* + version: link:../types + + packages/types: {} + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/plugin-virtual@3.0.2': + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@swc/core-darwin-arm64@1.15.33': + resolution: {integrity: sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.33': + resolution: {integrity: sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.33': + resolution: {integrity: sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.33': + resolution: {integrity: sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.33': + resolution: {integrity: sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.33': + resolution: {integrity: sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.33': + resolution: {integrity: sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.33': + resolution: {integrity: sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.33': + resolution: {integrity: sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.33': + resolution: {integrity: sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.33': + resolution: {integrity: sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.33': + resolution: {integrity: sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.33': + resolution: {integrity: sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@swc/wasm@1.15.33': + resolution: {integrity: sha512-uZPBvYMwjvTtyNm018KFV6ino5ZL4z9riN/tBsfTSgbfONW9Jn+ca88+UeEIdMOZY5Dm+y2OBf6o0kxa1wfD0A==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@typescript-eslint/eslint-plugin@8.59.4': + resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.4 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.4': + resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.4': + resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.4': + resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.4': + resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.4': + resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.4': + resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.4': + resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.4': + resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.4': + resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brotli-wasm@3.0.1: + resolution: {integrity: sha512-U3K72/JAi3jITpdhZBqzSUq+DUY697tLxOuFXB+FpAE/Ug+5C3VZrv4uA674EUZHxNAuQ9wETXNqQkxZD6oL4A==} + engines: {node: '>=v18.0.0'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.4: + resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-top-level-await@1.6.0: + resolution: {integrity: sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==} + peerDependencies: + vite: '>=2.8' + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.8.0 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.0 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.0(@types/node@25.9.1)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@25.9.1) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.0 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.0 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.3 + prettier: 2.8.8 + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/external-editor@1.0.3(@types/node@25.9.1)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.9.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.2 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/plugin-virtual@3.0.2(rollup@4.60.4)': + optionalDependencies: + rollup: 4.60.4 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@swc/core-darwin-arm64@1.15.33': + optional: true + + '@swc/core-darwin-x64@1.15.33': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.33': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.33': + optional: true + + '@swc/core-linux-arm64-musl@1.15.33': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.33': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-musl@1.15.33': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.33': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.33': + optional: true + + '@swc/core-win32-x64-msvc@1.15.33': + optional: true + + '@swc/core@1.15.33': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.33 + '@swc/core-darwin-x64': 1.15.33 + '@swc/core-linux-arm-gnueabihf': 1.15.33 + '@swc/core-linux-arm64-gnu': 1.15.33 + '@swc/core-linux-arm64-musl': 1.15.33 + '@swc/core-linux-ppc64-gnu': 1.15.33 + '@swc/core-linux-s390x-gnu': 1.15.33 + '@swc/core-linux-x64-gnu': 1.15.33 + '@swc/core-linux-x64-musl': 1.15.33 + '@swc/core-win32-arm64-msvc': 1.15.33 + '@swc/core-win32-ia32-msvc': 1.15.33 + '@swc/core-win32-x64-msvc': 1.15.33 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + + '@swc/wasm@1.15.33': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@12.20.55': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + + '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.4': {} + + '@typescript-eslint/typescript-estree@8.59.4(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.4(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + eslint-visitor-keys: 5.0.1 + + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.9.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@25.9.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.3(@types/node@25.9.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.3(@types/node@25.9.1) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brotli-wasm@3.0.1: {} + + cac@6.7.14: {} + + callsites@3.1.0: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.1.1: {} + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-module-lexer@1.7.0: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + extendable-error@0.1.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@14.0.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + html-escaper@2.0.2: {} + + human-id@4.1.3: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-tokens@10.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash.startcase@4.4.0: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.0 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + minipass@7.1.3: {} + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@4.0.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.8.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + term-size@2.2.1: {} + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.5 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.4(eslint@9.39.4)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + universalify@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@10.0.0: {} + + vite-node@3.2.4(@types/node@25.9.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.3(@types/node@25.9.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-top-level-await@1.6.0(rollup@4.60.4)(vite@7.3.3(@types/node@25.9.1)): + dependencies: + '@rollup/plugin-virtual': 3.0.2(rollup@4.60.4) + '@swc/core': 1.15.33 + '@swc/wasm': 1.15.33 + uuid: 10.0.0 + vite: 7.3.3(@types/node@25.9.1) + transitivePeerDependencies: + - '@swc/helpers' + - rollup + + vite-plugin-wasm@3.6.0(vite@7.3.3(@types/node@25.9.1)): + dependencies: + vite: 7.3.3(@types/node@25.9.1) + + vite@7.3.3(@types/node@25.9.1): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + + vitest@3.2.4(@types/node@25.9.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.3(@types/node@25.9.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.3(@types/node@25.9.1) + vite-node: 3.2.4(@types/node@25.9.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*"