From d0b9847c95e685fecfe872ac2e7bfb7c8c85bd51 Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 18 May 2026 23:45:22 -0300 Subject: [PATCH 01/35] chore: workspace + Changesets bootstrap (T-2) --- .changeset/config.json | 11 + .npmrc | 2 + CONTRIBUTING.md | 49 +++ package.json | 21 ++ pnpm-lock.yaml | 815 +++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + 6 files changed, 900 insertions(+) create mode 100644 .changeset/config.json create mode 100644 .npmrc create mode 100644 CONTRIBUTING.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml 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/.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/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/package.json b/package.json new file mode 100644 index 0000000..d8a82e7 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "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", + "typescript": "^5.6.0" + }, + "packageManager": "pnpm@10.24.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ae0aa21 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,815 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.27.0 + version: 2.31.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + +packages: + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@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==} + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@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'} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + 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'} + + 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'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + 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'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + 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'} + + 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'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + 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==} + + 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'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-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==} + + 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 + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + 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'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + 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-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + 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-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + 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-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + 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'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + 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@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'} + + 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'} + + 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'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + +snapshots: + + '@babel/runtime@7.29.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': + 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 + '@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 + + '@inquirer/external-editor@1.0.3': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + + '@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 + + '@types/node@12.20.55': {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chardet@2.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + esprima@4.0.1: {} + + extendable-error@0.1.7: {} + + 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 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + 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 + + 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 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + 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: {} + + human-id@4.1.3: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + 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: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mri@1.2.0: {} + + 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-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pify@4.0.1: {} + + prettier@2.8.8: {} + + 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@5.0.0: {} + + reusify@1.1.0: {} + + 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: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + term-size@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + typescript@5.9.3: {} + + universalify@0.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.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/*" From 31b78682acacc0b3146b862cbecbe1ec80e9883c Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 18 May 2026 23:48:33 -0300 Subject: [PATCH 02/35] =?UTF-8?q?feat(types):=20package=20skeleton=20?= =?UTF-8?q?=E2=80=94=20TS=20type=20declarations=20(T-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChainId union (5 EVM chains), NetworkConfig, PaymentProof, PaymentRequiredResponse, FrameContext, FrameState. Zero runtime deps. pnpm build produces dist/ with .js + .d.ts. npm pack --dry-run clean. --- packages/types/.npmignore | 2 ++ packages/types/README.md | 35 +++++++++++++++++++++++++++++++++++ packages/types/package.json | 24 ++++++++++++++++++++++++ packages/types/src/frame.ts | 12 ++++++++++++ packages/types/src/index.ts | 3 +++ packages/types/src/network.ts | 9 +++++++++ packages/types/src/x402.ts | 13 +++++++++++++ packages/types/tsconfig.json | 19 +++++++++++++++++++ pnpm-lock.yaml | 20 ++++++++++++++++++++ 9 files changed, 137 insertions(+) create mode 100644 packages/types/.npmignore create mode 100644 packages/types/README.md create mode 100644 packages/types/package.json create mode 100644 packages/types/src/frame.ts create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/src/network.ts create mode 100644 packages/types/src/x402.ts create mode 100644 packages/types/tsconfig.json 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 index ae0aa21..79f6662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,20 @@ importers: specifier: ^5.6.0 version: 5.9.3 + packages/codec: + dependencies: + brotli-wasm: + specifier: ^3.0.1 + version: 3.0.1 + + packages/networks: + dependencies: + '@void-layer/types': + specifier: workspace:* + version: link:../types + + packages/types: {} + packages: '@babel/runtime@7.29.2': @@ -132,6 +146,10 @@ packages: 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'} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -595,6 +613,8 @@ snapshots: dependencies: fill-range: 7.1.1 + brotli-wasm@3.0.1: {} + chardet@2.1.1: {} cross-spawn@7.0.6: From 5d4388fa950f3a60bb0e51c0fc47abdab720dc4a Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 18 May 2026 23:49:16 -0300 Subject: [PATCH 03/35] =?UTF-8?q?feat(networks):=20package=20skeleton=20?= =?UTF-8?q?=E2=80=94=205=20EVM=20chain=20configs=20(no=20RPC=20keys)=20(T-?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/networks/.npmignore | 2 ++ packages/networks/README.md | 36 ++++++++++++++++++++++++++++++ packages/networks/package.json | 27 +++++++++++++++++++++++ packages/networks/src/chains.ts | 39 +++++++++++++++++++++++++++++++++ packages/networks/src/index.ts | 4 ++++ packages/networks/src/rpc.ts | 8 +++++++ packages/networks/src/tokens.ts | 12 ++++++++++ packages/networks/tsconfig.json | 17 ++++++++++++++ 8 files changed, 145 insertions(+) create mode 100644 packages/networks/.npmignore create mode 100644 packages/networks/README.md create mode 100644 packages/networks/package.json create mode 100644 packages/networks/src/chains.ts create mode 100644 packages/networks/src/index.ts create mode 100644 packages/networks/src/rpc.ts create mode 100644 packages/networks/src/tokens.ts create mode 100644 packages/networks/tsconfig.json 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"] +} From 1de5be522c2e434de30a595626b0b89a864e41c0 Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 18 May 2026 23:55:01 -0300 Subject: [PATCH 04/35] =?UTF-8?q?feat(codec):=20package=20skeleton=20?= =?UTF-8?q?=E2=80=94=20Rust=20hello-world=20+=20wasm-pack=20manifest=20(T-?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins rust-toolchain to 1.85.0 (minimum for edition 2024; 1.84.1 from previous dispatch was incompatible). cargo build --release + cargo test (1/1) both green. npm pack --dry-run succeeds. --- packages/codec/.npmignore | 10 ++++++ packages/codec/Cargo.lock | 7 ++++ packages/codec/Cargo.toml | 21 +++++++++++ packages/codec/README.md | 52 ++++++++++++++++++++++++++++ packages/codec/REGISTRY.md | 44 +++++++++++++++++++++++ packages/codec/docs/bundle-budget.md | 5 +++ packages/codec/package.json | 30 ++++++++++++++++ packages/codec/rust-toolchain.toml | 5 +++ packages/codec/src/lib.rs | 16 +++++++++ packages/codec/vectors/.gitkeep | 0 10 files changed, 190 insertions(+) create mode 100644 packages/codec/.npmignore create mode 100644 packages/codec/Cargo.lock create mode 100644 packages/codec/Cargo.toml create mode 100644 packages/codec/README.md create mode 100644 packages/codec/REGISTRY.md create mode 100644 packages/codec/docs/bundle-budget.md create mode 100644 packages/codec/package.json create mode 100644 packages/codec/rust-toolchain.toml create mode 100644 packages/codec/src/lib.rs create mode 100644 packages/codec/vectors/.gitkeep 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..78f6939 --- /dev/null +++ b/packages/codec/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "void-layer-codec" +version = "0.0.0" diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml new file mode 100644 index 0000000..7433080 --- /dev/null +++ b/packages/codec/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "void-layer-codec" +version = "0.0.0" +edition = "2024" +license = "MIT" +description = "Canonical Invoice codec — TLV + Brotli wire format" +repository = "https://github.com/void-layer/codec" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +# Phase 2 adds: wasm-bindgen, serde, serde-wasm-bindgen, tsify, thiserror, phf, tiny-keccak + +[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..6678a31 --- /dev/null +++ b/packages/codec/docs/bundle-budget.md @@ -0,0 +1,5 @@ +# Bundle Budget + +Baseline TBD at Phase 2 first wasm-opt run. + +Hard limits per spec §3: WASM <80KB, package <200KB. diff --git a/packages/codec/package.json b/packages/codec/package.json new file mode 100644 index 0000000..ddbe35c --- /dev/null +++ b/packages/codec/package.json @@ -0,0 +1,30 @@ +{ + "name": "@void-layer/codec", + "version": "0.0.0", + "description": "Canonical Invoice codec — TLV + Brotli wire format. v1 schema LOCKED.", + "type": "module", + "exports": { + ".": { + "import": "./pkg/codec.js", + "require": "./cjs/index.js", + "types": "./pkg/codec.d.ts" + }, + "./types": "./pkg/codec.d.ts" + }, + "files": ["pkg/", "cjs/", "README.md", "LICENSE", "REGISTRY.md"], + "repository": { + "type": "git", + "url": "git+https://github.com/void-layer/codec.git", + "directory": "packages/codec" + }, + "license": "MIT", + "scripts": { + "build": "echo 'Phase 1 stub — wasm-pack invocation lands Phase 2'", + "test": "echo 'Phase 1 stub — Cargo+vitest land Phase 2'", + "lint": "echo 'Phase 1 stub'" + }, + "peerDependencies": { + "brotli-wasm": "^3.0.1" + }, + "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/src/lib.rs b/packages/codec/src/lib.rs new file mode 100644 index 0000000..f302e82 --- /dev/null +++ b/packages/codec/src/lib.rs @@ -0,0 +1,16 @@ +//! @void-layer/codec — Phase 1 scaffolding. Real impl lands Phase 2. +//! See spec 056 in voidpay-ai for full design. + +pub fn hello() -> &'static str { + "void-layer-codec phase 1" +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hello_works() { + assert_eq!(hello(), "void-layer-codec phase 1"); + } +} diff --git a/packages/codec/vectors/.gitkeep b/packages/codec/vectors/.gitkeep new file mode 100644 index 0000000..e69de29 From 969e5f8f56589c4124521e2a5b0f92d0eb9e76ed Mon Sep 17 00:00:00 2001 From: Ignat Date: Mon, 18 May 2026 23:59:04 -0300 Subject: [PATCH 05/35] =?UTF-8?q?ci:=20Phase=201=20scaffold=20=E2=80=94=20?= =?UTF-8?q?release.yml=20LOCKED=20filename=20+=20ci.yml=20+=20dependabot?= =?UTF-8?q?=20(T-6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODEOWNERS | 4 ++++ .github/dependabot.yml | 21 +++++++++++++++++++++ .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 18 ++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml 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..ca08a5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +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: 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 + - run: | + for dir in packages/codec packages/types packages/networks; do + (cd "$dir" && npm pack --dry-run) + done + 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 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)" From 73843735c9be0c20107d5b76608ea5709be35af9 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 00:02:18 -0300 Subject: [PATCH 06/35] =?UTF-8?q?docs:=20Phase=201=20documentation=20found?= =?UTF-8?q?ation=20=E2=80=94=20README=20+=20CoC=20+=20SECURITY=20+=20archi?= =?UTF-8?q?tecture=20(T-8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CODE_OF_CONDUCT.md | 23 +++++++++++ README.md | 59 +++++++++++++++++++++------- SECURITY.md | 41 ++++++++++++++++++++ docs/architecture-overview.md | 64 +++++++++++++++++++++++++++++++ docs/contributing-tlv-registry.md | 61 +++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md create mode 100644 docs/architecture-overview.md create mode 100644 docs/contributing-tlv-registry.md 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/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 From f0646657d7a48bb16753cbb1994b3d5613ebb833 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 00:03:44 -0300 Subject: [PATCH 07/35] lint: scaffold ESLint 9 flat + Prettier + clippy + rustfmt (T-7) --- .editorconfig | 15 +++++++++++++++ .prettierignore | 8 ++++++++ .prettierrc.json | 10 ++++++++++ eslint.config.mjs | 36 ++++++++++++++++++++++++++++++++++++ packages/codec/.clippy.toml | 2 ++ packages/codec/rustfmt.toml | 7 +++++++ 6 files changed, 78 insertions(+) create mode 100644 .editorconfig create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.mjs create mode 100644 packages/codec/.clippy.toml create mode 100644 packages/codec/rustfmt.toml 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/.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/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/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/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 From 53f43acb269ecf744f0daf3a73e26765887e8195 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 00:05:08 -0300 Subject: [PATCH 08/35] chore: persist ESLint deps in package.json + lockfile (T-7 follow-up) --- package.json | 5 +- pnpm-lock.yaml | 829 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 833 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d8a82e7..231e796 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ }, "devDependencies": { "@changesets/cli": "^2.27.0", - "typescript": "^5.6.0" + "@eslint/js": "^9.39.4", + "eslint": "^9.39.4", + "typescript": "^5.6.0", + "typescript-eslint": "^8.59.4" }, "packageManager": "pnpm@10.24.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79f6662..0a7fb0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: '@changesets/cli': specifier: ^2.27.0 version: 2.31.0 + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + eslint: + specifier: ^9.39.4 + version: 9.39.4 typescript: specifier: ^5.6.0 version: 5.9.3 + typescript-eslint: + specifier: ^8.59.4 + version: 8.59.4(eslint@9.39.4)(typescript@5.9.3) packages/codec: dependencies: @@ -90,6 +99,64 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@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'} @@ -117,9 +184,87 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@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==} + '@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} + + 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'} @@ -128,6 +273,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -138,10 +287,24 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + 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@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'} @@ -150,13 +313,43 @@ packages: resolution: {integrity: sha512-U3K72/JAi3jITpdhZBqzSUq+DUY697tLxOuFXB+FpAE/Ug+5C3VZrv4uA674EUZHxNAuQ9wETXNqQkxZD6oL4A==} engines: {node: '>=v18.0.0'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + 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-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'} @@ -169,21 +362,93 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + 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'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.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'} @@ -192,6 +457,17 @@ packages: 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==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -204,6 +480,14 @@ packages: 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'} + + 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'} @@ -211,6 +495,10 @@ packages: 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'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -223,6 +511,18 @@ packages: 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'} @@ -254,13 +554,36 @@ packages: 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==} @@ -272,10 +595,27 @@ packages: 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==} + 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==} + + 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==} @@ -287,10 +627,18 @@ packages: 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'} @@ -302,6 +650,10 @@ packages: 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'} @@ -321,15 +673,27 @@ packages: 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'} + 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==} @@ -340,6 +704,10 @@ packages: 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'} @@ -389,14 +757,43 @@ packages: 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'} + + 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'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.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@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -406,11 +803,22 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: '@babel/runtime@7.29.2': {} @@ -558,6 +966,68 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@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': dependencies: chardet: 2.1.1 @@ -591,12 +1061,124 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + '@types/node@12.20.55': {} + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@5.9.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@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) + '@typescript-eslint/types': 8.59.4 + debug: 4.4.3 + typescript: 5.9.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@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.4': {} + + '@typescript-eslint/typescript-estree@8.59.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.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@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.4(eslint@9.39.4)(typescript@5.9.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@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + eslint-visitor-keys: 5.0.1 + + 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-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -605,24 +1187,58 @@ snapshots: array-union@2.1.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@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 brotli-wasm@3.0.1: {} + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chardet@2.1.1: {} + 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-is@0.1.4: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: @@ -634,10 +1250,82 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + 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: {} + + esutils@2.0.3: {} + extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -646,10 +1334,22 @@ snapshots: 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 @@ -659,6 +1359,18 @@ snapshots: 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: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -675,6 +1387,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -686,6 +1404,8 @@ snapshots: graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + human-id@4.1.3: {} iconv-lite@0.7.2: @@ -694,6 +1414,15 @@ snapshots: 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-glob@4.0.3: @@ -719,14 +1448,35 @@ snapshots: 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: {} merge2@1.4.1: {} @@ -736,8 +1486,29 @@ snapshots: 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 + mri@1.2.0: {} + ms@2.1.3: {} + + 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: @@ -748,10 +1519,18 @@ snapshots: 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: {} @@ -760,6 +1539,10 @@ snapshots: 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: {} @@ -770,10 +1553,16 @@ snapshots: picomatch@2.3.2: {} + picomatch@4.0.4: {} + pify@4.0.1: {} + prelude-ls@1.2.1: {} + prettier@2.8.8: {} + punycode@2.3.1: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -785,6 +1574,8 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} reusify@1.1.0: {} @@ -820,16 +1611,54 @@ snapshots: strip-bom@3.0.0: {} + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + term-size@2.2.1: {} + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.4(eslint@9.39.4)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} universalify@0.1.2: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + which@2.0.2: dependencies: isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} From ae692e63daad5979b33ecc0a2040594e520f67bb Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 00:24:24 -0300 Subject: [PATCH 09/35] chore(codec): bump to 0.0.1 for crates.io name reservation --- packages/codec/Cargo.lock | 2 +- packages/codec/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock index 78f6939..4d3d212 100644 --- a/packages/codec/Cargo.lock +++ b/packages/codec/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "void-layer-codec" -version = "0.0.0" +version = "0.0.1" diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index 7433080..42f435b 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "void-layer-codec" -version = "0.0.0" +version = "0.0.1" edition = "2024" license = "MIT" description = "Canonical Invoice codec — TLV + Brotli wire format" From deebae52dcdf59a7541a1528f660c09f2478fe24 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:22:34 -0300 Subject: [PATCH 10/35] =?UTF-8?q?spike(brotli):=20measure=20compression=20?= =?UTF-8?q?+=20WASM=20blob=20=E2=80=94=20B-i=20ruled=20out,=20B-iv=20confi?= =?UTF-8?q?rmed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corpus: 20 synthetic invoices via TS reference codec (140–564 B uncompressed, median 193 B). Brotli-wasm q=11 median compressed: 185 B — Plan-C NOT triggered. WASM blob measurements (wasm-pack 0.13.1 + wasm-opt -Oz, Rust 1.85.0): A (brotli-decompressor decoder-only): ~196 KB blob / ~201 KB pkg total B (brotli v7 full encoder+decoder): ~953 KB blob / ~959 KB pkg total C (brotli v7 no-stdlib): ≈ B — no decoder-only feature gate in v7 Verdict: B-i RULED OUT. B-iv CONFIRMED — Rust ships brotli-decompressor only; encode-wire is native JS-side. Matches Ignat pre-decision. Cargo.toml unchanged (T-P2-1 owns deps). --- packages/codec/docs/spike-brotli-2026-05.md | 195 ++++++ .../codec/scripts/generate-spike-corpus.ts | 600 ++++++++++++++++++ .../spike-corpus/01-minimal-1item-evm.json | 7 + .../02-medium-2items-evm-notes.json | 7 + .../03-full-3items-evm-all-fields.json | 7 + .../04-minimal-1item-eth-mainnet.json | 7 + .../05-minimal-1item-polygon.json | 7 + .../spike-corpus/06-minimal-1item-base.json | 7 + .../07-minimal-1item-optimism.json | 7 + .../08-medium-2items-usdc-arb.json | 7 + .../09-medium-2items-no-notes.json | 7 + .../10-full-3items-client-wallet.json | 7 + .../11-full-3items-tax-discount.json | 7 + .../12-medium-2items-long-descriptions.json | 7 + .../13-minimal-1item-raw-currency.json | 7 + .../14-full-3items-all-optional-text.json | 7 + .../15-minimal-1item-small-amount.json | 7 + .../16-minimal-1item-large-amount.json | 7 + .../17-medium-2items-fractional-qty.json | 7 + .../18-full-3items-eip712-heavy.json | 7 + .../19-medium-2items-long-invoiceid.json | 7 + .../20-full-3items-both-emails.json | 7 + 22 files changed, 935 insertions(+) create mode 100644 packages/codec/docs/spike-brotli-2026-05.md create mode 100644 packages/codec/scripts/generate-spike-corpus.ts create mode 100644 packages/codec/vectors/spike-corpus/01-minimal-1item-evm.json create mode 100644 packages/codec/vectors/spike-corpus/02-medium-2items-evm-notes.json create mode 100644 packages/codec/vectors/spike-corpus/03-full-3items-evm-all-fields.json create mode 100644 packages/codec/vectors/spike-corpus/04-minimal-1item-eth-mainnet.json create mode 100644 packages/codec/vectors/spike-corpus/05-minimal-1item-polygon.json create mode 100644 packages/codec/vectors/spike-corpus/06-minimal-1item-base.json create mode 100644 packages/codec/vectors/spike-corpus/07-minimal-1item-optimism.json create mode 100644 packages/codec/vectors/spike-corpus/08-medium-2items-usdc-arb.json create mode 100644 packages/codec/vectors/spike-corpus/09-medium-2items-no-notes.json create mode 100644 packages/codec/vectors/spike-corpus/10-full-3items-client-wallet.json create mode 100644 packages/codec/vectors/spike-corpus/11-full-3items-tax-discount.json create mode 100644 packages/codec/vectors/spike-corpus/12-medium-2items-long-descriptions.json create mode 100644 packages/codec/vectors/spike-corpus/13-minimal-1item-raw-currency.json create mode 100644 packages/codec/vectors/spike-corpus/14-full-3items-all-optional-text.json create mode 100644 packages/codec/vectors/spike-corpus/15-minimal-1item-small-amount.json create mode 100644 packages/codec/vectors/spike-corpus/16-minimal-1item-large-amount.json create mode 100644 packages/codec/vectors/spike-corpus/17-medium-2items-fractional-qty.json create mode 100644 packages/codec/vectors/spike-corpus/18-full-3items-eip712-heavy.json create mode 100644 packages/codec/vectors/spike-corpus/19-medium-2items-long-invoiceid.json create mode 100644 packages/codec/vectors/spike-corpus/20-full-3items-both-emails.json 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/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/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" +} From 80860e3579587eb3015a7867d320475cdd0987ec Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:37:05 -0300 Subject: [PATCH 11/35] spike(bigint): WASM<->JS boundary failure-mode + regression test (D-B11) --- packages/codec/Cargo.lock | 409 ++++++++++++++++++ packages/codec/Cargo.toml | 8 +- .../docs/spike-bigint-boundary-2026-05.md | 75 ++++ packages/codec/tests/bigint_boundary.rs | 164 +++++++ 4 files changed, 655 insertions(+), 1 deletion(-) create mode 100644 packages/codec/docs/spike-bigint-boundary-2026-05.md create mode 100644 packages/codec/tests/bigint_boundary.rs diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock index 4d3d212..2f5f8b2 100644 --- a/packages/codec/Cargo.lock +++ b/packages/codec/Cargo.lock @@ -2,6 +2,415 @@ # It is not intended for manual editing. version = 4 +[[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 = "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 = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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 = "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 = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[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 = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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 = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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 = "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 = "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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + [[package]] name = "void-layer-codec" version = "0.0.1" +dependencies = [ + "js-sys", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[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 = "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 = "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 = "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 index 42f435b..3bd1ea9 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -10,7 +10,13 @@ repository = "https://github.com/void-layer/codec" crate-type = ["cdylib", "rlib"] [dependencies] -# Phase 2 adds: wasm-bindgen, serde, serde-wasm-bindgen, tsify, thiserror, phf, tiny-keccak +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" + +[dev-dependencies] +wasm-bindgen-test = "0.3" +js-sys = "0.3" [profile.release] opt-level = "z" 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/tests/bigint_boundary.rs b/packages/codec/tests/bigint_boundary.rs new file mode 100644 index 0000000..af0ed72 --- /dev/null +++ b/packages/codec/tests/bigint_boundary.rs @@ -0,0 +1,164 @@ +// 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 js_sys; +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); + } +} From 2e1179ff17a4072c7b87b59d818befbb5c008176 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:39:42 -0300 Subject: [PATCH 12/35] deps(codec): lock Phase 2 dependency set (T-P2-1) --- packages/codec/Cargo.lock | 695 +++++++++++++++++++++++++++++++++++++- packages/codec/Cargo.toml | 10 + 2 files changed, 704 insertions(+), 1 deletion(-) diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock index 2f5f8b2..bcb6eeb 100644 --- a/packages/codec/Cargo.lock +++ b/packages/codec/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[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" @@ -19,6 +40,37 @@ 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 = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -47,12 +99,63 @@ 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" @@ -77,6 +180,83 @@ dependencies = [ "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 = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[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" @@ -95,12 +275,36 @@ dependencies = [ "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" @@ -148,12 +352,73 @@ 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" @@ -163,6 +428,31 @@ 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" @@ -172,12 +462,108 @@ 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 = "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" @@ -187,6 +573,12 @@ 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" @@ -197,6 +589,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -228,6 +631,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -247,6 +661,12 @@ 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" @@ -264,23 +684,120 @@ dependencies = [ "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 = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[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.0.1" dependencies = [ + "brotli-decompressor", + "dlmalloc", "js-sys", + "phf", + "proptest", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.6.5", + "serde_json", + "thiserror", + "tiny-keccak", + "tsify", "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" @@ -291,6 +808,24 @@ dependencies = [ "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" @@ -385,6 +920,50 @@ 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 = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -409,6 +988,120 @@ 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" diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index 3bd1ea9..ec41fd4 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -13,10 +13,20 @@ crate-type = ["cdylib", "rlib"] wasm-bindgen = "0.2" serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" +serde_json = "1" +tsify = { version = "0.4", features = ["js"] } +thiserror = "2" +phf = { version = "0.11", features = ["macros"] } +tiny-keccak = { version = "2", features = ["keccak"] } +brotli-decompressor = "4" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +dlmalloc = { version = "0.2", features = ["global"] } [dev-dependencies] wasm-bindgen-test = "0.3" js-sys = "0.3" +proptest = "1" [profile.release] opt-level = "z" From 2cea71e50289375fd707e064cf2d4f6412eabdbd Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:44:54 -0300 Subject: [PATCH 13/35] =?UTF-8?q?fix(codec):=20drop=20redundant=20js=5Fsys?= =?UTF-8?q?=20import=20=E2=80=94=20clippy=20single-component-path=20(T-P2-?= =?UTF-8?q?0b=20follow-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/codec/tests/bigint_boundary.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/codec/tests/bigint_boundary.rs b/packages/codec/tests/bigint_boundary.rs index af0ed72..a7fecc9 100644 --- a/packages/codec/tests/bigint_boundary.rs +++ b/packages/codec/tests/bigint_boundary.rs @@ -8,7 +8,6 @@ // - 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 js_sys; use serde::Serialize; use wasm_bindgen::JsValue; use wasm_bindgen_test::*; From 6e7c5976138dfe9a8c80456c90344ea546efbed5 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:44:54 -0300 Subject: [PATCH 14/35] feat(codec): CodecError 9-variant error enum (T-P2-2) --- packages/codec/src/error.rs | 24 ++++++++++++ packages/codec/src/lib.rs | 3 ++ packages/codec/tests/error_display.rs | 55 +++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 packages/codec/src/error.rs create mode 100644 packages/codec/tests/error_display.rs diff --git a/packages/codec/src/error.rs b/packages/codec/src/error.rs new file mode 100644 index 0000000..250bbe7 --- /dev/null +++ b/packages/codec/src/error.rs @@ -0,0 +1,24 @@ +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), +} diff --git a/packages/codec/src/lib.rs b/packages/codec/src/lib.rs index f302e82..169bf69 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -1,6 +1,9 @@ //! @void-layer/codec — Phase 1 scaffolding. Real impl lands Phase 2. //! See spec 056 in voidpay-ai for full design. +pub mod error; +pub use error::CodecError; + pub fn hello() -> &'static str { "void-layer-codec phase 1" } diff --git a/packages/codec/tests/error_display.rs b/packages/codec/tests/error_display.rs new file mode 100644 index 0000000..d65add8 --- /dev/null +++ b/packages/codec/tests/error_display.rs @@ -0,0 +1,55 @@ +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"); +} From 76cc7934b59fa5202d230081b67c55914cf107d1 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:50:17 -0300 Subject: [PATCH 15/35] feat(codec): LEB128 varint primitive, MAX_BYTES=37 (T-P2-3) --- packages/codec/src/lib.rs | 2 + packages/codec/src/varint.rs | 288 +++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 packages/codec/src/varint.rs diff --git a/packages/codec/src/lib.rs b/packages/codec/src/lib.rs index 169bf69..193937a 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -4,6 +4,8 @@ pub mod error; pub use error::CodecError; +pub(crate) mod varint; + pub fn hello() -> &'static str { "void-layer-codec phase 1" } diff --git a/packages/codec/src/varint.rs b/packages/codec/src/varint.rs new file mode 100644 index 0000000..ae22237 --- /dev/null +++ b/packages/codec/src/varint.rs @@ -0,0 +1,288 @@ +// 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.last().unwrap() == 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()); + } + } + + 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); + } + } +} From 74100d0bb5c3334580a68907444f13ed6893d354 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:53:37 -0300 Subject: [PATCH 16/35] =?UTF-8?q?fix(codec):=20drop=20guarded=20unwrap=20i?= =?UTF-8?q?n=20shr7=5Fle=20=E2=80=94=20AC-15=20no-unwrap=20discipline=20(T?= =?UTF-8?q?-P2-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/codec/src/varint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codec/src/varint.rs b/packages/codec/src/varint.rs index ae22237..d9f5ddb 100644 --- a/packages/codec/src/varint.rs +++ b/packages/codec/src/varint.rs @@ -180,7 +180,7 @@ fn shr7_le(le: &mut Vec) { carry = val & 0x7F; } // Trim trailing zero bytes (which are the most-significant in LE). - while le.len() > 1 && *le.last().unwrap() == 0 { + while le.len() > 1 && le[le.len() - 1] == 0 { le.pop(); } } From c3a8accb1c312c1a6ef693701ea266de05ca3e82 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 21:58:05 -0300 Subject: [PATCH 17/35] feat(codec): TLV record primitives, BTreeMap byte-stable (T-P2-4) --- packages/codec/src/lib.rs | 1 + packages/codec/src/tlv.rs | 277 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 packages/codec/src/tlv.rs diff --git a/packages/codec/src/lib.rs b/packages/codec/src/lib.rs index 193937a..13d19c5 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub use error::CodecError; pub(crate) mod varint; +pub(crate) mod tlv; pub fn hello() -> &'static str { "void-layer-codec phase 1" 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:?}" + ); + } +} From ec17a0e2aa1ae90a955a97fcde462060cca5930d Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 22:02:09 -0300 Subject: [PATCH 18/35] feat(codec): phf static dictionaries app + chain (T-P2-5) --- packages/codec/src/dict/app.rs | 24 +++++++ packages/codec/src/dict/chain.rs | 20 ++++++ packages/codec/src/dict/mod.rs | 117 +++++++++++++++++++++++++++++++ packages/codec/src/lib.rs | 1 + 4 files changed, 162 insertions(+) create mode 100644 packages/codec/src/dict/app.rs create mode 100644 packages/codec/src/dict/chain.rs create mode 100644 packages/codec/src/dict/mod.rs 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..9ec1a97 --- /dev/null +++ b/packages/codec/src/dict/mod.rs @@ -0,0 +1,117 @@ +// 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 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 = ""; + const CHAIN_DICT_HASH: &str = ""; + + fn keccak256_hex(data: &[u8]) -> String { + let mut k = Keccak::v256(); + let mut out = [0u8; 32]; + k.update(data); + k.finalize(&mut out); + out.iter().map(|b| format!("{b:02x}")).collect() + } + + /// 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); + out.iter().map(|b| format!("{b:02x}")).collect() + } + + /// 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); + out.iter().map(|b| format!("{b:02x}")).collect() + } + + #[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/lib.rs b/packages/codec/src/lib.rs index 13d19c5..f82bd17 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -6,6 +6,7 @@ pub use error::CodecError; pub(crate) mod varint; pub(crate) mod tlv; +pub(crate) mod dict; pub fn hello() -> &'static str { "void-layer-codec phase 1" From 00f1feb977a79d898c9d915637ac98e25ddb3f85 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 22:04:43 -0300 Subject: [PATCH 19/35] test(codec): lock dictionary snapshot hashes (T-P2-5) --- packages/codec/src/dict/mod.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/codec/src/dict/mod.rs b/packages/codec/src/dict/mod.rs index 9ec1a97..0955deb 100644 --- a/packages/codec/src/dict/mod.rs +++ b/packages/codec/src/dict/mod.rs @@ -9,19 +9,27 @@ pub(crate) mod chain; 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 = ""; - const CHAIN_DICT_HASH: &str = ""; + 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); - out.iter().map(|b| format!("{b:02x}")).collect() + to_hex(&out) } /// Hash all APP_DICT entries: for each entry iterate sorted keys, @@ -36,7 +44,7 @@ mod tests { } let mut out = [0u8; 32]; k.finalize(&mut out); - out.iter().map(|b| format!("{b:02x}")).collect() + to_hex(&out) } /// Hash all CHAIN_DICT entries: iterate sorted keys, @@ -51,7 +59,7 @@ mod tests { } let mut out = [0u8; 32]; k.finalize(&mut out); - out.iter().map(|b| format!("{b:02x}")).collect() + to_hex(&out) } #[test] From c19d645cb33bf6af344bd6f241e627f70ba62235 Mon Sep 17 00:00:00 2001 From: Ignat Date: Tue, 19 May 2026 22:07:41 -0300 Subject: [PATCH 20/35] feat(codec): keccak256 content-hash primitive (T-P2-6) --- packages/codec/src/hash.rs | 71 ++++++++++++++++++++++++++++++++++++++ packages/codec/src/lib.rs | 3 ++ 2 files changed, 74 insertions(+) create mode 100644 packages/codec/src/hash.rs 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/lib.rs b/packages/codec/src/lib.rs index f82bd17..3290202 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -8,6 +8,9 @@ pub(crate) mod varint; pub(crate) mod tlv; pub(crate) mod dict; +pub(crate) mod hash; +pub use hash::compute_content_hash; + pub fn hello() -> &'static str { "void-layer-codec phase 1" } From 6f9d0a1cb7c244955b994d5b089aa5e390d1aee6 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 02:08:38 -0300 Subject: [PATCH 21/35] =?UTF-8?q?chore(codec):=20drop=20brotli-decompresso?= =?UTF-8?q?r=20dep=20=E2=80=94=20B-iv=20superseded=20by=20B-v=20replan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T-P2-7 deleted. B-iv (decode in Rust) measured at ~196 KB WASM blob vs the 80 KB hard cap. brotli-decompressor mandates a ~120 KB static dictionary with no decoder-only feature gate. B-v LOCKED: WASM ships TLV+keccak core only. Both compress and decompress live in the JS shim layer over brotli-wasm peerDep. Wire bytes unchanged (brotli-wasm q11 = same compressor as TS codec). Removes: brotli-decompressor v4.0.3, alloc-stdlib v0.2.2, alloc-no-stdlib v2.0.4 Cargo.lock staged with Cargo.toml per Phase 1 rule F-3. --- packages/codec/Cargo.lock | 26 -------------------------- packages/codec/Cargo.toml | 3 +-- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock index bcb6eeb..3fd9034 100644 --- a/packages/codec/Cargo.lock +++ b/packages/codec/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -61,16 +46,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "brotli-decompressor" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.20.2" @@ -774,7 +749,6 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" name = "void-layer-codec" version = "0.0.1" dependencies = [ - "brotli-decompressor", "dlmalloc", "js-sys", "phf", diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index ec41fd4..ce8647a 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -3,7 +3,7 @@ name = "void-layer-codec" version = "0.0.1" edition = "2024" license = "MIT" -description = "Canonical Invoice codec — TLV + Brotli wire format" +description = "Canonical Invoice codec — TLV wire format, Brotli via JS shim" repository = "https://github.com/void-layer/codec" [lib] @@ -18,7 +18,6 @@ tsify = { version = "0.4", features = ["js"] } thiserror = "2" phf = { version = "0.11", features = ["macros"] } tiny-keccak = { version = "2", features = ["keccak"] } -brotli-decompressor = "4" [target.'cfg(target_arch = "wasm32")'.dependencies] dlmalloc = { version = "0.2", features = ["global"] } From 7759eb371ede0eb5d7dbf1f4e5820c7c750b5654 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 02:28:55 -0300 Subject: [PATCH 22/35] fix(codec): resolve clippy::format_collect warnings in proptest helpers Replace `map(|b| format!("{b:02x}")).collect::()` with `fold + write!` pattern in arb_wallet_address and arb_invoice. cargo clippy --all-targets --all-features -- -D warnings now exits 0. --- packages/codec/tests/codec_smoke.rs | 344 ++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 packages/codec/tests/codec_smoke.rs diff --git a/packages/codec/tests/codec_smoke.rs b/packages/codec/tests/codec_smoke.rs new file mode 100644 index 0000000..7929a24 --- /dev/null +++ b/packages/codec/tests/codec_smoke.rs @@ -0,0 +1,344 @@ +use void_layer_codec::{ + decode_invoice_canonical, encode_invoice_canonical, CodecError, Invoice, InvoiceClient, + InvoiceFrom, InvoiceItem, +}; + +// --------------------------------------------------------------------------- +// 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); + } +} From 2b69236bb268bd750bed6d8347eb6850af0888fd Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 02:29:13 -0300 Subject: [PATCH 23/35] feat(codec): B-v canonical encode/decode WASM layer (T-P2-7-alt) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the canonical-only Rust surface per B-v replan: - encode_invoice_canonical → TLV bytes, COMPRESSED_FLAG never set - decode_invoice_canonical → Invoice from canonical bytes, rejects 0x80 - lib.rs: exports 2 canonical fns + compute_content_hash; no wire variants - wasm.rs: exactly 2 #[wasm_bindgen] exports (encodeInvoiceCanonical / decodeInvoiceCanonical); BigInt-safe via serde_large_number_types_as_bigints - invoice.rs: Invoice/InvoiceFrom/InvoiceClient/InvoiceItem with Tsify + serde - encode.rs: TLV type registry, phf dict, mantissa/LEB128 encoding, domain sep - decode.rs: full TLV decode, domain separator verify, BigInt-safe mantissa No brotli dep in Rust. Wire compression lives in JS shim (src/index.ts). --- packages/codec/src/decode.rs | 720 ++++++++++++++++++++++++++++++++++ packages/codec/src/encode.rs | 628 +++++++++++++++++++++++++++++ packages/codec/src/invoice.rs | 94 +++++ packages/codec/src/lib.rs | 45 ++- packages/codec/src/wasm.rs | 50 +++ 5 files changed, 1521 insertions(+), 16 deletions(-) create mode 100644 packages/codec/src/decode.rs create mode 100644 packages/codec/src/encode.rs create mode 100644 packages/codec/src/invoice.rs create mode 100644 packages/codec/src/wasm.rs diff --git a/packages/codec/src/decode.rs b/packages/codec/src/decode.rs new file mode 100644 index 0000000..d024651 --- /dev/null +++ b/packages/codec/src/decode.rs @@ -0,0 +1,720 @@ +// 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_DUE_AT, TLV_DOMAIN_SEPARATOR, 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 + // Convert big-endian bytes → u128 (sufficient for USDC/ETH amounts) + let mut mantissa: u128 = 0; + for b in &mantissa_bytes { + mantissa = mantissa.checked_shl(8).unwrap_or(u128::MAX) | (*b as u128); + } + let scale: u128 = 10u128.pow(zeros); + let value = mantissa.saturating_mul(scale); + 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" + ))); + } + + let mut mantissa: u128 = 0; + for b in &mantissa_be { + mantissa = mantissa.checked_shl(8).unwrap_or(u128::MAX) | (*b as u128); + } + let rate = mantissa.saturating_mul(10u128.pow(zeros)).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, + }) +} + +// --------------------------------------------------------------------------- +// 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"); + } +} diff --git a/packages/codec/src/encode.rs b/packages/codec/src/encode.rs new file mode 100644 index 0000000..e6a17a7 --- /dev/null +++ b/packages/codec/src/encode.rs @@ -0,0 +1,628 @@ +// 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. +fn mantissa_bytes(value_str: &str) -> Result, CodecError> { + // Parse as u128 — enough for USDC/ETH amounts in atomic units + let value: u128 = value_str + .parse() + .map_err(|_| CodecError::CompressionFailed(format!("invalid amount: {value_str}")))?; + + let mut buf = Vec::new(); + if value == 0 { + // mantissa = 0 (single 0x00 byte), zeros = 0 + write_bigint_varint(&[0], &mut buf); + buf.push(0); + return Ok(buf); + } + + let mut mantissa = value; + let mut zeros: u8 = 0; + while mantissa % 10 == 0 { + mantissa /= 10; + zeros += 1; + } + // Write mantissa as big-endian bytes via bigint_varint + let mantissa_be = 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) +} + +// --------------------------------------------------------------------------- +// 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"); + } +} diff --git a/packages/codec/src/invoice.rs b/packages/codec/src/invoice.rs new file mode 100644 index 0000000..f5c532f --- /dev/null +++ b/packages/codec/src/invoice.rs @@ -0,0 +1,94 @@ +use serde::{Deserialize, Serialize}; +use tsify::Tsify; + +/// A single line item in an invoice. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +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, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +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, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +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, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +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 index 3290202..731395e 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -1,26 +1,39 @@ -//! @void-layer/codec — Phase 1 scaffolding. Real impl lands Phase 2. +//! @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 use error::CodecError; +pub mod invoice; pub(crate) mod varint; pub(crate) mod tlv; pub(crate) mod dict; - pub(crate) mod hash; -pub use hash::compute_content_hash; +pub(crate) mod encode; +pub(crate) mod decode; -pub fn hello() -> &'static str { - "void-layer-codec phase 1" -} +#[cfg(target_arch = "wasm32")] +mod wasm; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hello_works() { - assert_eq!(hello(), "void-layer-codec phase 1"); - } -} +// --- Canonical public surface --- +pub use error::CodecError; +pub use invoice::{Invoice, InvoiceClient, InvoiceFrom, InvoiceItem}; +pub use encode::encode_invoice_canonical; +pub use decode::decode_invoice_canonical; +pub use hash::compute_content_hash; diff --git a/packages/codec/src/wasm.rs b/packages/codec/src/wasm.rs new file mode 100644 index 0000000..317e694 --- /dev/null +++ b/packages/codec/src/wasm.rs @@ -0,0 +1,50 @@ +//! WASM bindings — compiled only for `target_arch = "wasm32"`. +//! +//! Exports exactly 2 functions to JS per B-v replan (2026-05-20): +//! - `encodeInvoiceCanonical` — TLV canonical bytes (no Brotli) +//! - `decodeInvoiceCanonical` — Invoice from canonical bytes +//! +//! 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::{decode_invoice_canonical, encode_invoice_canonical, Invoice}; + +/// 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())) +} + +/// dlmalloc allocator — ~5 KB overhead, replaces the default (wee_alloc is forbidden per §3.8). +#[global_allocator] +static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; From ba52e5bb767dcf7a67e10bb2c2711ebd400c7218 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 02:34:37 -0300 Subject: [PATCH 24/35] refactor(codec): drop tsify + serde_json from production deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed tsify (gloo-utils, web-sys, serde_json) and serde_json from [dependencies] — not needed since wasm.rs uses JsValue + serde_wasm_bindgen directly. Invoice structs keep Serialize+Deserialize for serde_wasm_bindgen. WASM blob after wasm-pack release build: 163 KB (hard cap 80 KB exceeded). Size checkpoint FAILED — see T-P2-7-alt final report for Kai escalation. --- packages/codec/Cargo.lock | 75 +---------------------------------- packages/codec/Cargo.toml | 2 - packages/codec/src/invoice.rs | 13 ++---- 3 files changed, 5 insertions(+), 85 deletions(-) diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock index 3fd9034..8678fdf 100644 --- a/packages/codec/Cargo.lock +++ b/packages/codec/Cargo.lock @@ -180,19 +180,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "gloo-utils" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -564,17 +551,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-wasm-bindgen" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -606,17 +582,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_derive_internals" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_json" version = "1.0.149" @@ -701,32 +666,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tsify" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" -dependencies = [ - "gloo-utils", - "serde", - "serde-wasm-bindgen 0.5.0", - "serde_json", - "tsify-macros", - "wasm-bindgen", -] - -[[package]] -name = "tsify-macros" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - [[package]] name = "unarray" version = "0.1.4" @@ -754,11 +693,9 @@ dependencies = [ "phf", "proptest", "serde", - "serde-wasm-bindgen 0.6.5", - "serde_json", + "serde-wasm-bindgen", "thiserror", "tiny-keccak", - "tsify", "wasm-bindgen", "wasm-bindgen-test", ] @@ -928,16 +865,6 @@ dependencies = [ "semver", ] -[[package]] -name = "web-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi-util" version = "0.1.11" diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index ce8647a..238f248 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -13,8 +13,6 @@ crate-type = ["cdylib", "rlib"] wasm-bindgen = "0.2" serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" -serde_json = "1" -tsify = { version = "0.4", features = ["js"] } thiserror = "2" phf = { version = "0.11", features = ["macros"] } tiny-keccak = { version = "2", features = ["keccak"] } diff --git a/packages/codec/src/invoice.rs b/packages/codec/src/invoice.rs index f5c532f..dd0ea77 100644 --- a/packages/codec/src/invoice.rs +++ b/packages/codec/src/invoice.rs @@ -1,9 +1,7 @@ use serde::{Deserialize, Serialize}; -use tsify::Tsify; /// A single line item in an invoice. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InvoiceItem { /// Human-readable description of the line item. pub description: String, @@ -14,8 +12,7 @@ pub struct InvoiceItem { } /// Originator (payee) contact details. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InvoiceFrom { /// Display name of the issuer. pub name: String, @@ -32,8 +29,7 @@ pub struct InvoiceFrom { } /// Client (payer) contact details. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InvoiceClient { /// Display name of the client. pub name: String, @@ -53,8 +49,7 @@ pub struct InvoiceClient { /// /// 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, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Invoice { /// Unique invoice identifier (e.g. "INV-001"). pub invoice_id: String, From a1e6753d80b0158672c4ebb341113b69887a085f Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 03:13:06 -0300 Subject: [PATCH 25/35] =?UTF-8?q?feat(codec):=20B-v=20JS=20shim=20?= =?UTF-8?q?=E2=80=94=20wire=20encode/decode=20over=20brotli-wasm=20(T-P2-7?= =?UTF-8?q?-alt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/index.ts: 4-name public API — encodeInvoiceCanonical/decodeInvoiceCanonical re-exported from WASM, encodeInvoiceWire/decodeInvoiceWire over brotli-wasm peerDep (COMPRESSED_FLAG + expand-fallback mirror reference compressPayload) - vitest.config.ts: vite-plugin-wasm + top-level-await; brotli-wasm aliased to the CJS node build for the Node test env - package.json: dist/ shim is the main entry (was raw WASM pkg); build runs wasm-pack then tsc, strips wasm-pack's pkg/.gitignore so pkg/ ships - 6 shim tests green (canonical + wire roundtrip, COMPRESSED_FLAG set/clear) --- packages/codec/package.json | 32 +- packages/codec/src/index.test.ts | 104 +++ packages/codec/src/index.ts | 95 +++ packages/codec/tsconfig.json | 19 + packages/codec/vitest.config.ts | 23 + pnpm-lock.yaml | 1126 +++++++++++++++++++++++++++++- 6 files changed, 1389 insertions(+), 10 deletions(-) create mode 100644 packages/codec/src/index.test.ts create mode 100644 packages/codec/src/index.ts create mode 100644 packages/codec/tsconfig.json create mode 100644 packages/codec/vitest.config.ts diff --git a/packages/codec/package.json b/packages/codec/package.json index ddbe35c..65ec649 100644 --- a/packages/codec/package.json +++ b/packages/codec/package.json @@ -3,15 +3,16 @@ "version": "0.0.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": { ".": { - "import": "./pkg/codec.js", - "require": "./cjs/index.js", - "types": "./pkg/codec.d.ts" - }, - "./types": "./pkg/codec.d.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "files": ["pkg/", "cjs/", "README.md", "LICENSE", "REGISTRY.md"], + "files": ["dist/", "pkg/", "README.md", "LICENSE", "REGISTRY.md"], "repository": { "type": "git", "url": "git+https://github.com/void-layer/codec.git", @@ -19,12 +20,25 @@ }, "license": "MIT", "scripts": { - "build": "echo 'Phase 1 stub — wasm-pack invocation lands Phase 2'", - "test": "echo 'Phase 1 stub — Cargo+vitest land Phase 2'", - "lint": "echo 'Phase 1 stub'" + "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", + "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": { + "brotli-wasm": "^3.0.1", + "typescript": "^5.9.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/src/index.test.ts b/packages/codec/src/index.test.ts new file mode 100644 index 0000000..a067f6c --- /dev/null +++ b/packages/codec/src/index.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from 'vitest' +import { + encodeInvoiceCanonical, + decodeInvoiceCanonical, + encodeInvoiceWire, + decodeInvoiceWire, +} from './index.js' + +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) + 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) + 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) + 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) + expect(decoded.invoice_id).toBe('INV-001') + }) +}) diff --git a/packages/codec/src/index.ts b/packages/codec/src/index.ts new file mode 100644 index 0000000..330f7b7 --- /dev/null +++ b/packages/codec/src/index.ts @@ -0,0 +1,95 @@ +/** + * @void-layer/codec JS shim — public entry point. + * + * Exposes 4 functions: + * - encodeInvoiceCanonical / decodeInvoiceCanonical (WASM canonical, no Brotli) + * - 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 the 2 canonical WASM functions directly. +export { + encodeInvoiceCanonical, + decodeInvoiceCanonical, +} 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/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/vitest.config.ts b/packages/codec/vitest.config.ts new file mode 100644 index 0000000..a33ec33 --- /dev/null +++ b/packages/codec/vitest.config.ts @@ -0,0 +1,23 @@ +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', + }, + 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/pnpm-lock.yaml b/pnpm-lock.yaml index 0a7fb0c..8d90c7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,10 +25,22 @@ importers: version: 8.59.4(eslint@9.39.4)(typescript@5.9.3) packages/codec: - dependencies: + devDependencies: brotli-wasm: specifier: ^3.0.1 version: 3.0.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite-plugin-top-level-await: + specifier: ^1.4.4 + version: 1.6.0(rollup@4.60.4)(vite@7.3.3) + vite-plugin-wasm: + specifier: ^3.4.1 + version: 3.6.0(vite@7.3.3) + vitest: + specifier: ^3.0.0 + version: 3.2.4 packages/networks: dependencies: @@ -99,6 +111,162 @@ packages: '@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} @@ -166,6 +334,9 @@ packages: '@types/node': optional: true + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -184,6 +355,239 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@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==} @@ -252,6 +656,35 @@ packages: resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@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: @@ -287,6 +720,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -313,10 +750,18 @@ packages: 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'} @@ -324,6 +769,10 @@ packages: 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'} @@ -347,6 +796,10 @@ packages: 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==} @@ -362,6 +815,14 @@ packages: 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'} @@ -413,10 +874,17 @@ packages: 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==} @@ -476,6 +944,11 @@ packages: 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'} @@ -546,6 +1019,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + 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 @@ -587,6 +1063,12 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -609,6 +1091,11 @@ packages: 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==} @@ -666,6 +1153,13 @@ packages: 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==} @@ -681,6 +1175,10 @@ packages: 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'} @@ -716,6 +1214,11 @@ packages: 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==} @@ -735,6 +1238,9 @@ packages: 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'} @@ -743,12 +1249,22 @@ packages: 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==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -761,6 +1277,9 @@ packages: 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'} @@ -769,10 +1288,28 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + 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'} @@ -806,11 +1343,104 @@ packages: 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'} @@ -966,6 +1596,84 @@ snapshots: 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 @@ -1033,6 +1741,8 @@ snapshots: chardet: 2.1.1 iconv-lite: 0.7.2 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -1061,6 +1771,156 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@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': {} @@ -1158,6 +2018,48 @@ snapshots: '@typescript-eslint/types': 8.59.4 eslint-visitor-keys: 5.0.1 + '@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)': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.3 + + '@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 @@ -1187,6 +2089,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -1210,8 +2114,18 @@ snapshots: 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 @@ -1219,6 +2133,8 @@ snapshots: chardet@2.1.1: {} + check-error@2.1.3: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -1237,6 +2153,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} detect-indent@6.1.0: {} @@ -1250,6 +2168,37 @@ snapshots: 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: @@ -1320,8 +2269,14 @@ snapshots: 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: {} @@ -1383,6 +2338,9 @@ snapshots: 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 @@ -1439,6 +2397,8 @@ snapshots: isexe@2.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -1479,6 +2439,12 @@ snapshots: lodash.startcase@4.4.0: {} + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} micromatch@4.0.8: @@ -1498,6 +2464,8 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.12: {} + natural-compare@1.4.0: {} optionator@0.9.4: @@ -1549,6 +2517,10 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -1557,6 +2529,12 @@ snapshots: 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: {} @@ -1580,6 +2558,37 @@ snapshots: 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 @@ -1594,10 +2603,14 @@ snapshots: 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 @@ -1605,6 +2618,10 @@ snapshots: sprintf-js@1.0.3: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -1613,17 +2630,31 @@ snapshots: 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: {} + 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 @@ -1655,10 +2686,103 @@ snapshots: dependencies: punycode: 2.3.1 + uuid@10.0.0: {} + + vite-node@3.2.4: + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.3 + 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): + 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 + transitivePeerDependencies: + - '@swc/helpers' + - rollup + + vite-plugin-wasm@3.6.0(vite@7.3.3): + dependencies: + vite: 7.3.3 + + vite@7.3.3: + 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: + fsevents: 2.3.3 + + vitest@3.2.4: + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.3) + '@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 + vite-node: 3.2.4 + why-is-node-running: 2.3.0 + 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: {} yocto-queue@0.1.0: {} From f51851c5470776ee9c1053e8017c5ba204e9d5e1 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 03:28:25 -0300 Subject: [PATCH 26/35] =?UTF-8?q?style(codec):=20cargo=20fmt=20=E2=80=94?= =?UTF-8?q?=20resolve=20fmt=20--check=20CI=20gate=20(Iris=20Gate=20A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iris Gate A2 flagged cargo fmt --check failures across 5 files (purely stylistic — line wrapping, import ordering, no logic change). fmt now clean; 81 Rust tests still green. --- packages/codec/src/decode.rs | 50 ++++++++++++------------- packages/codec/src/dict/mod.rs | 15 +++++--- packages/codec/src/encode.rs | 35 +++++++++-------- packages/codec/src/lib.rs | 14 +++---- packages/codec/src/varint.rs | 11 +++++- packages/codec/src/wasm.rs | 6 +-- packages/codec/tests/bigint_boundary.rs | 47 ++++++++++++++--------- packages/codec/tests/codec_smoke.rs | 9 +++-- packages/codec/tests/error_display.rs | 5 ++- 9 files changed, 113 insertions(+), 79 deletions(-) diff --git a/packages/codec/src/decode.rs b/packages/codec/src/decode.rs index d024651..a365539 100644 --- a/packages/codec/src/decode.rs +++ b/packages/codec/src/decode.rs @@ -9,12 +9,11 @@ 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_DUE_AT, TLV_DOMAIN_SEPARATOR, 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, + 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; @@ -134,13 +133,13 @@ static CURRENCY_CODE_TO_SYMBOL: &[(u8, &str)] = &[ /// 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"), + (1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + (2, "0xdac17f958d2ee523a2206206994597c13d831ec7"), + (3, "0x6b175474e89094c44da98b954eedeac495271d0f"), + (4, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + (5, "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599"), + (6, "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c"), + (7, "0x6c96de32cea08842dcc4058c14d3aaad7fa41dee"), (10, "0xaf88d065e77c8cc2239327c5edb3a432268e5831"), (11, "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"), (12, "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"), @@ -404,7 +403,8 @@ pub fn decode_invoice_canonical(bytes: &[u8]) -> Result { // 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(), + "unexpected compressed input in decode_invoice_canonical — decompress first" + .to_string(), )); } if version_byte != VERSION { @@ -479,10 +479,9 @@ pub fn decode_invoice_canonical(bytes: &[u8]) -> Result { 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 decimals = *decimals_bytes + .first() + .ok_or(CodecError::Truncated { needed: 1, had: 0 })?; let from_wallet_bytes = records .get(&TLV_FROM_WALLET) @@ -597,15 +596,14 @@ pub fn decode_invoice_canonical(bytes: &[u8]) -> Result { None }; - let discount = if let Some(v) = records.get(&TLV_DISCOUNT) { - Some( - String::from_utf8(v.clone()).map_err(|_| { + 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 - }; + })?) + } else { + None + }; Ok(Invoice { invoice_id, diff --git a/packages/codec/src/dict/mod.rs b/packages/codec/src/dict/mod.rs index 0955deb..9ff889f 100644 --- a/packages/codec/src/dict/mod.rs +++ b/packages/codec/src/dict/mod.rs @@ -15,7 +15,8 @@ mod tests { // 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"; + const CHAIN_DICT_HASH: &str = + "6ddf0a04233a8b0b6dffe4658782eb5bd13391b37d202894e4da66efc5b388da"; fn to_hex(bytes: &[u8]) -> String { bytes.iter().fold(String::new(), |mut acc, b| { @@ -95,7 +96,11 @@ mod tests { #[test] fn chain_dict_entry_count() { - assert_eq!(CHAIN_DICT.len(), 5, "CHAIN_DICT must have exactly 5 entries"); + assert_eq!( + CHAIN_DICT.len(), + 5, + "CHAIN_DICT must have exactly 5 entries" + ); } #[test] @@ -108,9 +113,9 @@ mod tests { #[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 + 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] diff --git a/packages/codec/src/encode.rs b/packages/codec/src/encode.rs index e6a17a7..88924fe 100644 --- a/packages/codec/src/encode.rs +++ b/packages/codec/src/encode.rs @@ -73,8 +73,8 @@ fn address_to_bytes(address: &str) -> Result<[u8; 20], CodecError> { } 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)?; + out[i] = + u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| CodecError::BadMagic)?; } Ok(out) } @@ -200,11 +200,11 @@ static TOKEN_ADDRESS_TO_CODE: &[(&str, u8)] = &[ /// Chain ID → (code_min, code_max) range for token dict validation. static CHAIN_CODE_RANGES: &[(u32, u8, u8)] = &[ - (1, 1, 9), + (1, 1, 9), (42161, 10, 19), - (10, 20, 29), - (137, 30, 39), - (8453, 40, 49), + (10, 20, 29), + (137, 30, 39), + (8453, 40, 49), ]; /// Encode currency per spec §5.1: @@ -212,7 +212,10 @@ static CHAIN_CODE_RANGES: &[(u32, u8, u8)] = &[ /// 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()) { + if let Some(&(_, code)) = CURRENCY_SYMBOL_TO_CODE + .iter() + .find(|&&(k, _)| k == upper.as_str()) + { vec![0x00, code] } else { let mut val = vec![0x01]; @@ -227,16 +230,18 @@ fn encode_currency(currency: &str) -> Vec { 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()) { + 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 effective_code = + if addr_lower == "0x4200000000000000000000000000000000000006" && network_id == 8453 { + 43u8 + } else { + code + }; let in_range = CHAIN_CODE_RANGES .iter() diff --git a/packages/codec/src/lib.rs b/packages/codec/src/lib.rs index 731395e..04ef3cc 100644 --- a/packages/codec/src/lib.rs +++ b/packages/codec/src/lib.rs @@ -21,19 +21,19 @@ pub mod error; pub mod invoice; -pub(crate) mod varint; -pub(crate) mod tlv; +pub(crate) mod decode; pub(crate) mod dict; -pub(crate) mod hash; pub(crate) mod encode; -pub(crate) mod decode; +pub(crate) mod hash; +pub(crate) mod tlv; +pub(crate) mod varint; #[cfg(target_arch = "wasm32")] mod wasm; // --- Canonical public surface --- -pub use error::CodecError; -pub use invoice::{Invoice, InvoiceClient, InvoiceFrom, InvoiceItem}; -pub use encode::encode_invoice_canonical; 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/varint.rs b/packages/codec/src/varint.rs index d9f5ddb..c18ad4d 100644 --- a/packages/codec/src/varint.rs +++ b/packages/codec/src/varint.rs @@ -100,7 +100,10 @@ pub(crate) fn write_bigint_varint(value: &[u8], out: &mut Vec) { /// 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> { +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; @@ -252,7 +255,11 @@ mod tests { 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"); + assert_eq!( + bytes_consumed, + buf.len(), + "bytes_consumed must equal full buffer" + ); } #[test] diff --git a/packages/codec/src/wasm.rs b/packages/codec/src/wasm.rs index 317e694..74f97a5 100644 --- a/packages/codec/src/wasm.rs +++ b/packages/codec/src/wasm.rs @@ -13,7 +13,7 @@ use serde::Serialize; use serde_wasm_bindgen::Serializer; use wasm_bindgen::prelude::*; -use crate::{decode_invoice_canonical, encode_invoice_canonical, Invoice}; +use crate::{Invoice, 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). @@ -29,8 +29,8 @@ fn ts_serializer() -> Serializer { /// 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()))?; + 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())) } diff --git a/packages/codec/tests/bigint_boundary.rs b/packages/codec/tests/bigint_boundary.rs index a7fecc9..fb8a8ab 100644 --- a/packages/codec/tests/bigint_boundary.rs +++ b/packages/codec/tests/bigint_boundary.rs @@ -75,8 +75,8 @@ fn config_a_u64_max_returns_err() { #[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 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(), @@ -86,10 +86,13 @@ fn config_b_above_2_53_is_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 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"); + 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!( @@ -100,8 +103,8 @@ fn config_b_u64_max_is_exact_bigint() { #[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 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(), @@ -115,23 +118,25 @@ fn config_b_safe_53_is_still_bigint() { 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"); + let back = js_val + .as_string() + .expect("String amount must round-trip as JS string"); assert_eq!( - back, - "115792089237316195423570985008687907853269984665640564039457584007913129639935", + 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 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"); + let back = js_val + .as_string() + .expect("String amount must round-trip as JS string under Config B"); assert_eq!( - back, - "115792089237316195423570985008687907853269984665640564039457584007913129639935", + back, "115792089237316195423570985008687907853269984665640564039457584007913129639935", "String amount (uint256::MAX) must survive Config B unchanged" ); } @@ -148,7 +153,11 @@ fn zod_refine_accepts_valid_integer_strings() { ]; 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); + assert!( + result.is_ok(), + "Zod refine: '{}' must be accepted by BigInt(v)", + s + ); } } @@ -158,6 +167,10 @@ fn zod_refine_rejects_invalid_strings() { 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); + 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 index 7929a24..82579ef 100644 --- a/packages/codec/tests/codec_smoke.rs +++ b/packages/codec/tests/codec_smoke.rs @@ -1,6 +1,6 @@ use void_layer_codec::{ - decode_invoice_canonical, encode_invoice_canonical, CodecError, Invoice, InvoiceClient, - InvoiceFrom, InvoiceItem, + CodecError, Invoice, InvoiceClient, InvoiceFrom, InvoiceItem, decode_invoice_canonical, + encode_invoice_canonical, }; // --------------------------------------------------------------------------- @@ -234,7 +234,10 @@ fn tlv_count_byte_matches_actual_tlv_count() { 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}"); + assert!( + tlv_count >= 13, + "minimal invoice should have at least 13 TLV records, got {tlv_count}" + ); } // --------------------------------------------------------------------------- diff --git a/packages/codec/tests/error_display.rs b/packages/codec/tests/error_display.rs index d65add8..be80576 100644 --- a/packages/codec/tests/error_display.rs +++ b/packages/codec/tests/error_display.rs @@ -20,7 +20,10 @@ fn unknown_extension_displays_type() { #[test] fn dictionary_mismatch_displays_expected_and_actual() { - let err = CodecError::DictionaryMismatch { expected: 1, actual: 2 }; + let err = CodecError::DictionaryMismatch { + expected: 1, + actual: 2, + }; assert_eq!(err.to_string(), "dictionary mismatch: expected 1, actual 2"); } From 575517dc467664f1e0a1ed5bf38855cb4e9368da Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 14:26:54 -0300 Subject: [PATCH 27/35] feat(codec): bridge receiptHash JS export (T-P2-9b) - wasm.rs: #[wasm_bindgen(js_name = receiptHash)] export - wasm_boundary.rs: boundary test (32-byte digest, deterministic) - index.ts: re-export receiptHash - Decision: receipt_hash ships in Phase 2 (plan-2c C6, Ignat 2026-05-20) --- packages/codec/src/index.ts | 6 ++-- packages/codec/src/wasm.rs | 16 +++++++-- packages/codec/tests/wasm_boundary.rs | 51 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 packages/codec/tests/wasm_boundary.rs diff --git a/packages/codec/src/index.ts b/packages/codec/src/index.ts index 330f7b7..5fa04be 100644 --- a/packages/codec/src/index.ts +++ b/packages/codec/src/index.ts @@ -1,8 +1,9 @@ /** * @void-layer/codec JS shim — public entry point. * - * Exposes 4 functions: + * 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. @@ -11,10 +12,11 @@ import type { BrotliWasmType } from 'brotli-wasm' -// Re-export the 2 canonical WASM functions directly. +// Re-export canonical WASM functions directly. export { encodeInvoiceCanonical, decodeInvoiceCanonical, + receiptHash, } from '../pkg/void_layer_codec.js' // --------------------------------------------------------------------------- diff --git a/packages/codec/src/wasm.rs b/packages/codec/src/wasm.rs index 74f97a5..c68788b 100644 --- a/packages/codec/src/wasm.rs +++ b/packages/codec/src/wasm.rs @@ -1,8 +1,9 @@ //! WASM bindings — compiled only for `target_arch = "wasm32"`. //! -//! Exports exactly 2 functions to JS per B-v replan (2026-05-20): +//! 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. @@ -13,7 +14,7 @@ use serde::Serialize; use serde_wasm_bindgen::Serializer; use wasm_bindgen::prelude::*; -use crate::{Invoice, decode_invoice_canonical, encode_invoice_canonical}; +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). @@ -45,6 +46,17 @@ pub fn decode_invoice_canonical_js(bytes: &[u8]) -> Result { .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/wasm_boundary.rs b/packages/codec/tests/wasm_boundary.rs new file mode 100644 index 0000000..fa7ff8f --- /dev/null +++ b/packages/codec/tests/wasm_boundary.rs @@ -0,0 +1,51 @@ +// WASM boundary test for receiptHash (compute_content_hash JS export). +// Task T-P2-9b — Phase 2B hotfix (2026-05-20). +// +// Tests: +// - 32-byte digest length +// - Determinism: same input → identical output across two calls + +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_node_experimental); + +#[wasm_bindgen(module = "/pkg/void_layer_codec.js")] +extern "C" { + #[wasm_bindgen(js_name = receiptHash, catch)] + fn receipt_hash_js(bytes: &[u8]) -> Result, JsValue>; +} + +/// 32-byte digest: receiptHash over a hand-crafted canonical TLV fixture +/// must return exactly 32 bytes. +#[wasm_bindgen_test] +fn receipt_hash_returns_32_bytes() { + // Hand-crafted canonical TLV: tag=0x01, length=0x03, value=[0xAA, 0xBB, 0xCC] + let canonical: &[u8] = &[0x01, 0x03, 0xAA, 0xBB, 0xCC]; + let digest = receipt_hash_js(canonical).expect("receiptHash must not trap"); + assert_eq!(digest.len(), 32, "receiptHash must return exactly 32 bytes"); +} + +/// Determinism: same canonical bytes → identical digest on two independent calls. +#[wasm_bindgen_test] +fn receipt_hash_is_deterministic() { + let canonical: &[u8] = &[0x01, 0x03, 0xAA, 0xBB, 0xCC]; + let first = receipt_hash_js(canonical).expect("first call must not trap"); + let second = receipt_hash_js(canonical).expect("second call must not trap"); + assert_eq!( + first, second, + "receiptHash 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 receipt_hash_distinct_for_distinct_inputs() { + let a = receipt_hash_js(&[0x01, 0x03, 0xAA, 0xBB, 0xCC]).expect("call A must not trap"); + let b = receipt_hash_js(&[]).expect("call B (empty) must not trap"); + assert_ne!( + a, b, + "receiptHash of distinct inputs must differ (non-constant function)" + ); +} From 1cf710dc2edbb36dbfd207cacab9c872fea6fbc2 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 14:37:42 -0300 Subject: [PATCH 28/35] =?UTF-8?q?fix(codec):=20make=20proptest=20target-co?= =?UTF-8?q?nditional=20=E2=80=94=20unblock=20wasm-pack=20test=20(T-P2-9c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - proptest -> [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] - getrandom 0.3 / wait-timeout don't build for wasm32; proptest must leave the wasm32 test graph - cfg-gate proptest-using test files to cfg(not(wasm32)) - wasm-pack test --node now compiles -> AC-9 boundary tests executable - see spec 056 plan-2c C8 --- packages/codec/Cargo.toml | 3 +++ packages/codec/src/varint.rs | 1 + packages/codec/tests/codec_smoke.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index 238f248..d348295 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -23,6 +23,9 @@ dlmalloc = { version = "0.2", features = ["global"] } [dev-dependencies] wasm-bindgen-test = "0.3" js-sys = "0.3" + +# 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] diff --git a/packages/codec/src/varint.rs b/packages/codec/src/varint.rs index c18ad4d..d8c058d 100644 --- a/packages/codec/src/varint.rs +++ b/packages/codec/src/varint.rs @@ -283,6 +283,7 @@ mod tests { } } + #[cfg(not(target_arch = "wasm32"))] proptest::proptest! { #[test] fn varint_roundtrips_for_any_u64(value in proptest::prelude::any::()) { diff --git a/packages/codec/tests/codec_smoke.rs b/packages/codec/tests/codec_smoke.rs index 82579ef..3f81bcd 100644 --- a/packages/codec/tests/codec_smoke.rs +++ b/packages/codec/tests/codec_smoke.rs @@ -1,3 +1,5 @@ +#![cfg(not(target_arch = "wasm32"))] + use void_layer_codec::{ CodecError, Invoice, InvoiceClient, InvoiceFrom, InvoiceItem, decode_invoice_canonical, encode_invoice_canonical, From 71a2a93ecd6398bf3327ee731b6d3a5d9434571c Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 14:45:30 -0300 Subject: [PATCH 29/35] fix(codec): wasm_boundary.rs calls compute_content_hash directly (T-P2-9b-fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drop #[wasm_bindgen(module = "/pkg/...")] extern block — a wasm_bindgen_test must not re-import the built JS bundle - call void_layer_codec::compute_content_hash directly, like bigint_boundary.rs - add receiptHash JS-export coverage to index.test.ts - fix pre-existing TS18046 errors on decoded: unknown (add DecodedInvoice cast) - fixes ERR_MODULE_NOT_FOUND under wasm-pack test --node --- packages/codec/src/index.test.ts | 29 ++++++++++++-- packages/codec/tests/wasm_boundary.rs | 55 +++++++++++++-------------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/packages/codec/src/index.test.ts b/packages/codec/src/index.test.ts index a067f6c..325b5df 100644 --- a/packages/codec/src/index.test.ts +++ b/packages/codec/src/index.test.ts @@ -4,8 +4,16 @@ import { 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, @@ -52,7 +60,7 @@ describe('encodeInvoiceCanonical + decodeInvoiceCanonical (WASM pass-through)', it('roundtrips through canonical encode → decode', () => { const bytes = encodeInvoiceCanonical(MINIMAL_INVOICE) - const decoded = decodeInvoiceCanonical(bytes) + const decoded = decodeInvoiceCanonical(bytes) as DecodedInvoice expect(decoded.invoice_id).toBe('INV-001') expect(decoded.currency).toBe('USDC') expect(decoded.total).toBe('1000000') @@ -77,7 +85,7 @@ describe('encodeInvoiceWire', () => { const wire = await encodeInvoiceWire(MINIMAL_INVOICE) expect(wire[1]! & 0x80).toBe(0) // and it must still roundtrip through decodeInvoiceWire - const decoded = await decodeInvoiceWire(wire) + const decoded = (await decodeInvoiceWire(wire)) as DecodedInvoice expect(decoded.invoice_id).toBe('INV-001') }) }) @@ -85,7 +93,7 @@ describe('encodeInvoiceWire', () => { describe('decodeInvoiceWire', () => { it('roundtrips through wire encode → decode', async () => { const wire = await encodeInvoiceWire(MINIMAL_INVOICE) - const decoded = await decodeInvoiceWire(wire) + const decoded = (await decodeInvoiceWire(wire)) as DecodedInvoice expect(decoded.invoice_id).toBe('INV-001') expect(decoded.currency).toBe('USDC') expect(decoded.total).toBe('1000000') @@ -98,7 +106,20 @@ describe('decodeInvoiceWire', () => { // 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) + 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/tests/wasm_boundary.rs b/packages/codec/tests/wasm_boundary.rs index fa7ff8f..f86acd3 100644 --- a/packages/codec/tests/wasm_boundary.rs +++ b/packages/codec/tests/wasm_boundary.rs @@ -1,51 +1,48 @@ -// WASM boundary test for receiptHash (compute_content_hash JS export). -// Task T-P2-9b — Phase 2B hotfix (2026-05-20). +// 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: -// - 32-byte digest length -// - Determinism: same input → identical output across two calls +// - 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::prelude::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_node_experimental); -#[wasm_bindgen(module = "/pkg/void_layer_codec.js")] -extern "C" { - #[wasm_bindgen(js_name = receiptHash, catch)] - fn receipt_hash_js(bytes: &[u8]) -> Result, JsValue>; -} - -/// 32-byte digest: receiptHash over a hand-crafted canonical TLV fixture -/// must return exactly 32 bytes. -#[wasm_bindgen_test] -fn receipt_hash_returns_32_bytes() { - // Hand-crafted canonical TLV: tag=0x01, length=0x03, value=[0xAA, 0xBB, 0xCC] - let canonical: &[u8] = &[0x01, 0x03, 0xAA, 0xBB, 0xCC]; - let digest = receipt_hash_js(canonical).expect("receiptHash must not trap"); - assert_eq!(digest.len(), 32, "receiptHash must return exactly 32 bytes"); -} - /// Determinism: same canonical bytes → identical digest on two independent calls. #[wasm_bindgen_test] -fn receipt_hash_is_deterministic() { +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 = receipt_hash_js(canonical).expect("first call must not trap"); - let second = receipt_hash_js(canonical).expect("second call must not trap"); + let first = void_layer_codec::compute_content_hash(canonical); + let second = void_layer_codec::compute_content_hash(canonical); assert_eq!( first, second, - "receiptHash must be deterministic — same input must yield identical digest" + "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 receipt_hash_distinct_for_distinct_inputs() { - let a = receipt_hash_js(&[0x01, 0x03, 0xAA, 0xBB, 0xCC]).expect("call A must not trap"); - let b = receipt_hash_js(&[]).expect("call B (empty) must not trap"); +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, - "receiptHash of distinct inputs must differ (non-constant function)" + "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" ); } From 56e9845c67f5d49d5cc040853afafc3e2b210e75 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 14:51:00 -0300 Subject: [PATCH 30/35] fix(ci): install wasm-pack in lint-and-build job - codec build script invokes `wasm-pack build` since B-v (a1e6753); lint-and-build ran `pnpm -r build` without wasm-pack on the runner - pin wasm-pack 0.14.1 per Phase 1 D-A5 - add explicit `rustup target add wasm32-unknown-unknown` before install - fixes CI run 26179858633 'wasm-pack: not found' --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca08a5e..2d49ea5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,9 @@ jobs: - 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 From 616ccb488e050b78e088f66f20aea668997525a2 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 19:18:56 -0300 Subject: [PATCH 31/35] feat(codec): widen amount domain u128 -> U256 (T-P2-12a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mantissa_bytes parses amounts as U256 (ruint) — matches the TS BigInt reference; covers the on-chain uint256 domain - decode reconstructs U256; removes the silent u128::MAX saturation bug - InvalidAmount error variant replaces miscategorised CompressionFailed - byte-identical output for amounts <= u128::MAX (parity preserved) - decision: codec amount domain = U256 (Ignat 2026-05-20) --- packages/codec/Cargo.toml | 1 + packages/codec/src/decode.rs | 102 ++++++++++++++++++++++++++++++----- packages/codec/src/encode.rs | 86 ++++++++++++++++++++++++++--- packages/codec/src/error.rs | 2 + 4 files changed, 171 insertions(+), 20 deletions(-) diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index d348295..eb8e04b 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -15,6 +15,7 @@ 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] diff --git a/packages/codec/src/decode.rs b/packages/codec/src/decode.rs index a365539..b1e1ee5 100644 --- a/packages/codec/src/decode.rs +++ b/packages/codec/src/decode.rs @@ -231,14 +231,21 @@ fn decode_mantissa(bytes: &[u8]) -> Result { ))); } - // Reconstruct value: mantissa_bytes is big-endian - // Convert big-endian bytes → u128 (sufficient for USDC/ETH amounts) - let mut mantissa: u128 = 0; - for b in &mantissa_bytes { - mantissa = mantissa.checked_shl(8).unwrap_or(u128::MAX) | (*b as u128); - } - let scale: u128 = 10u128.pow(zeros); - let value = mantissa.saturating_mul(scale); + // 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()) } @@ -306,11 +313,21 @@ fn unpack_items(data: &[u8]) -> Result, CodecError> { ))); } - let mut mantissa: u128 = 0; - for b in &mantissa_be { - mantissa = mantissa.checked_shl(8).unwrap_or(u128::MAX) | (*b as u128); + 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 rate = mantissa.saturating_mul(10u128.pow(zeros)).to_string(); + 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, @@ -638,6 +655,19 @@ pub fn decode_invoice_canonical(bytes: &[u8]) -> Result { }) } +// --------------------------------------------------------------------------- +// 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 // --------------------------------------------------------------------------- @@ -715,4 +745,52 @@ mod tests { 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/encode.rs b/packages/codec/src/encode.rs index 88924fe..8786481 100644 --- a/packages/codec/src/encode.rs +++ b/packages/codec/src/encode.rs @@ -93,28 +93,30 @@ fn varint_bytes(value: u64) -> Vec { /// 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> { - // Parse as u128 — enough for USDC/ETH amounts in atomic units - let value: u128 = value_str - .parse() - .map_err(|_| CodecError::CompressionFailed(format!("invalid amount: {value_str}")))?; + 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 == 0 { + 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 % 10 == 0 { - mantissa /= 10; + while mantissa % ten == U256::ZERO { + mantissa /= ten; zeros += 1; } // Write mantissa as big-endian bytes via bigint_varint - let mantissa_be = mantissa.to_be_bytes(); + let mantissa_be: [u8; 32] = mantissa.to_be_bytes(); write_bigint_varint(&mantissa_be, &mut buf); buf.push(zeros); Ok(buf) @@ -525,6 +527,19 @@ fn hex_decode_salt(hex: &str) -> Result, CodecError> { 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 // --------------------------------------------------------------------------- @@ -630,4 +645,59 @@ mod tests { 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 index 250bbe7..281c871 100644 --- a/packages/codec/src/error.rs +++ b/packages/codec/src/error.rs @@ -21,4 +21,6 @@ pub enum CodecError { ChecksumMismatch, #[error("compression failed: {0}")] CompressionFailed(String), + #[error("invalid amount: {0}")] + InvalidAmount(String), } From e7b03408483a20839645c9d40ae0e6cdf57bfe11 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 19:33:46 -0300 Subject: [PATCH 32/35] feat(codec): 17 golden vectors v4-codec.json (T-P2-12, schema_version=1) - 1 minimal + 5 chain-selectors + 4 bigint-edges + 3 extensions + 4 malformed - bigint edges: 0, 1, uint256-max (encodes), over-u256 (InvalidAmount) - TS generator over the U256 codec; Kai-reviewed (2 rounds) - two malformed subtypes: decode-input (hex) + encode-input (payload) - generator excluded from `pnpm test` (vitest scripts/** exclude) - append-only forever; schema_version locked at 1 per D-R6.1 Co-Authored-By: kai-cto --- packages/codec/docs/golden-vectors.md | 167 +++++++ packages/codec/package.json | 14 +- .../codec/scripts/generate-vectors.config.ts | 24 + packages/codec/scripts/generate-vectors.ts | 407 ++++++++++++++++ .../scripts/run-generate-vectors.test.ts | 16 + packages/codec/vectors/v4-codec.json | 438 ++++++++++++++++++ packages/codec/vitest.config.ts | 5 +- 7 files changed, 1068 insertions(+), 3 deletions(-) create mode 100644 packages/codec/docs/golden-vectors.md create mode 100644 packages/codec/scripts/generate-vectors.config.ts create mode 100644 packages/codec/scripts/generate-vectors.ts create mode 100644 packages/codec/scripts/run-generate-vectors.test.ts create mode 100644 packages/codec/vectors/v4-codec.json diff --git a/packages/codec/docs/golden-vectors.md b/packages/codec/docs/golden-vectors.md new file mode 100644 index 0000000..ad142ad --- /dev/null +++ b/packages/codec/docs/golden-vectors.md @@ -0,0 +1,167 @@ +# 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) + +17 vectors, regenerated 2026-05-20 for U256 widening (T-P2-12a / C9 amendment), +pending Kai review before commit. + +| # | 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-varint-overflow` | Malformed | — | +| 12 | `extension-magic-dust` | Extension | **true** | +| 13 | `extension-og-param` | Extension | **true** | +| 14 | `extension-sub-invoice-chain` | Extension | false | +| 15 | `malformed-corrupted-brotli` | Malformed | — | +| 16 | `malformed-oversize` | Malformed | — | +| 17 | `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. +- `bigint-overflow` renamed `malformed-varint-overflow` and reclassified to the + malformed category. It is a crafted varint byte stream (VarintOverflow is a decoder + error, not an amount-domain error). Hex is byte-identical to the pre-rework vector. + +**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/package.json b/packages/codec/package.json index 65ec649..fd08311 100644 --- a/packages/codec/package.json +++ b/packages/codec/package.json @@ -12,7 +12,13 @@ "import": "./dist/index.js" } }, - "files": ["dist/", "pkg/", "README.md", "LICENSE", "REGISTRY.md"], + "files": [ + "dist/", + "pkg/", + "README.md", + "LICENSE", + "REGISTRY.md" + ], "repository": { "type": "git", "url": "git+https://github.com/void-layer/codec.git", @@ -27,6 +33,7 @@ "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" }, @@ -34,11 +41,14 @@ "brotli-wasm": "^3.0.1" }, "devDependencies": { + "@types/node": "25.9.1", "brotli-wasm": "^3.0.1", "typescript": "^5.9.3", "vite-plugin-top-level-await": "^1.4.4", "vite-plugin-wasm": "^3.4.1", "vitest": "^3.0.0" }, - "engines": { "node": ">=24" } + "engines": { + "node": ">=24" + } } 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..9e070cb --- /dev/null +++ b/packages/codec/scripts/generate-vectors.ts @@ -0,0 +1,407 @@ +/** + * 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-varint-overflow — crafted bytes that produce VarintOverflow on decode. + // Valid magic+version+count=1 header, TLV_TOTAL=24, length=38, then 38 bytes + // all with continuation bit set (38 > MAX_BYTES=37 → VarintOverflow). + // Reclassified from 'bigint-overflow' (was misnamed — this is a malformed varint stream). + { + const overflowBytes = new Uint8Array([ + 0x56, 0x01, // MAGIC, VERSION + 0x01, // COUNT=1 + 0x18, // TLV type=24 (TOTAL) + 0x26, // length=38 + ...Array(38).fill(0x80), // 38 continuation bytes + ]) + vectors.push({ + name: 'malformed-varint-overflow', + canonical_hex: toHex(overflowBytes), + 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/vectors/v4-codec.json b/packages/codec/vectors/v4-codec.json new file mode 100644 index 0000000..51b6ddc --- /dev/null +++ b/packages/codec/vectors/v4-codec.json @@ -0,0 +1,438 @@ +{ + "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-varint-overflow", + "canonical_hex": "56010118268080808080808080808080808080808080808080808080808080808080808080808080808080", + "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 index a33ec33..8814e11 100644 --- a/packages/codec/vitest.config.ts +++ b/packages/codec/vitest.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vitest/config' +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' @@ -9,6 +9,9 @@ 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: { From 57440dda8aa6282428f26bd36af5cfa1310505fc Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 19:56:31 -0300 Subject: [PATCH 33/35] fix(codec): correct malformed vector set + pnpm-lock straggler (T-P2-12 follow-up) - malformed-varint-overflow relabelled: the old hex triggers ChecksumMismatch (no valid domain-separator TLV), not VarintOverflow -> kept as new vector `malformed-checksum-mismatch` - new `malformed-varint-overflow`: overflow moved into the TLV length-varint, hit during structural decode before the checksum check -> genuinely yields VarintOverflow (Kai-reviewed) - 18 golden vectors total - pnpm-lock.yaml: @types/node lockfile entry missed by e7b0340 --- packages/codec/docs/golden-vectors.md | 31 ++++++----- packages/codec/scripts/generate-vectors.ts | 44 +++++++++++----- packages/codec/vectors/v4-codec.json | 8 ++- pnpm-lock.yaml | 60 ++++++++++++++-------- 4 files changed, 98 insertions(+), 45 deletions(-) diff --git a/packages/codec/docs/golden-vectors.md b/packages/codec/docs/golden-vectors.md index ad142ad..6595798 100644 --- a/packages/codec/docs/golden-vectors.md +++ b/packages/codec/docs/golden-vectors.md @@ -78,8 +78,8 @@ assert that `encodeInvoiceCanonical` throws the named error variant. ## Starter Set (v4-codec.json, schema_version=1) -17 vectors, regenerated 2026-05-20 for U256 widening (T-P2-12a / C9 amendment), -pending Kai review before commit. +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 | |---|------|----------|----------------| @@ -93,13 +93,14 @@ pending Kai review before commit. | 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-varint-overflow` | Malformed | — | -| 12 | `extension-magic-dust` | Extension | **true** | -| 13 | `extension-og-param` | Extension | **true** | -| 14 | `extension-sub-invoice-chain` | Extension | false | -| 15 | `malformed-corrupted-brotli` | Malformed | — | -| 16 | `malformed-oversize` | Malformed | — | -| 17 | `malformed-bad-magic` | Malformed | — | +| 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 = @@ -107,9 +108,15 @@ pending Kai review before commit. 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. -- `bigint-overflow` renamed `malformed-varint-overflow` and reclassified to the - malformed category. It is a crafted varint byte stream (VarintOverflow is a decoder - error, not an amount-domain error). Hex is byte-identical to the pre-rework vector. + +**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 diff --git a/packages/codec/scripts/generate-vectors.ts b/packages/codec/scripts/generate-vectors.ts index 9e070cb..78885bd 100644 --- a/packages/codec/scripts/generate-vectors.ts +++ b/packages/codec/scripts/generate-vectors.ts @@ -251,21 +251,41 @@ async function main(): Promise { } as MalformedVector) } - // 3e. malformed-varint-overflow — crafted bytes that produce VarintOverflow on decode. - // Valid magic+version+count=1 header, TLV_TOTAL=24, length=38, then 38 bytes - // all with continuation bit set (38 > MAX_BYTES=37 → VarintOverflow). - // Reclassified from 'bigint-overflow' (was misnamed — this is a malformed varint stream). + // 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 overflowBytes = new Uint8Array([ - 0x56, 0x01, // MAGIC, VERSION - 0x01, // COUNT=1 - 0x18, // TLV type=24 (TOTAL) - 0x26, // length=38 - ...Array(38).fill(0x80), // 38 continuation bytes - ]) + 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(overflowBytes), + canonical_hex: toHex(buf), diagnostic: 'malformed:canonical', expected_error: 'VarintOverflow', }) diff --git a/packages/codec/vectors/v4-codec.json b/packages/codec/vectors/v4-codec.json index 51b6ddc..23c7241 100644 --- a/packages/codec/vectors/v4-codec.json +++ b/packages/codec/vectors/v4-codec.json @@ -312,9 +312,15 @@ "expected_error": "InvalidAmount" }, { - "name": "malformed-varint-overflow", + "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" }, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d90c7f..f76031b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.27.0 - version: 2.31.0 + version: 2.31.0(@types/node@25.9.1) '@eslint/js': specifier: ^9.39.4 version: 9.39.4 @@ -26,6 +26,9 @@ importers: packages/codec: devDependencies: + '@types/node': + specifier: 25.9.1 + version: 25.9.1 brotli-wasm: specifier: ^3.0.1 version: 3.0.1 @@ -34,13 +37,13 @@ importers: version: 5.9.3 vite-plugin-top-level-await: specifier: ^1.4.4 - version: 1.6.0(rollup@4.60.4)(vite@7.3.3) + 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) + version: 3.6.0(vite@7.3.3(@types/node@25.9.1)) vitest: specifier: ^3.0.0 - version: 3.2.4 + version: 3.2.4(@types/node@25.9.1) packages/networks: dependencies: @@ -597,6 +600,9 @@ packages: '@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} @@ -1336,6 +1342,9 @@ packages: 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'} @@ -1482,7 +1491,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.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 @@ -1498,7 +1507,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3 + '@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 @@ -1736,10 +1745,12 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/external-editor@1.0.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 '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1927,6 +1938,10 @@ snapshots: '@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@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2026,13 +2041,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.3)': + '@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 + vite: 7.3.3(@types/node@25.9.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2680,6 +2695,8 @@ snapshots: typescript@5.9.3: {} + undici-types@7.24.6: {} + universalify@0.1.2: {} uri-js@4.4.1: @@ -2688,13 +2705,13 @@ snapshots: uuid@10.0.0: {} - vite-node@3.2.4: + 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 + vite: 7.3.3(@types/node@25.9.1) transitivePeerDependencies: - '@types/node' - jiti @@ -2709,22 +2726,22 @@ snapshots: - tsx - yaml - vite-plugin-top-level-await@1.6.0(rollup@4.60.4)(vite@7.3.3): + 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 + vite: 7.3.3(@types/node@25.9.1) transitivePeerDependencies: - '@swc/helpers' - rollup - vite-plugin-wasm@3.6.0(vite@7.3.3): + vite-plugin-wasm@3.6.0(vite@7.3.3(@types/node@25.9.1)): dependencies: - vite: 7.3.3 + vite: 7.3.3(@types/node@25.9.1) - vite@7.3.3: + vite@7.3.3(@types/node@25.9.1): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -2733,13 +2750,14 @@ snapshots: rollup: 4.60.4 tinyglobby: 0.2.16 optionalDependencies: + '@types/node': 25.9.1 fsevents: 2.3.3 - vitest@3.2.4: + 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) + '@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 @@ -2757,9 +2775,11 @@ snapshots: tinyglobby: 0.2.16 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.3 - vite-node: 3.2.4 + 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 From 0088201a1c086523081cdc99532f5a5c79476712 Mon Sep 17 00:00:00 2001 From: Ignat Date: Wed, 20 May 2026 19:56:40 -0300 Subject: [PATCH 34/35] =?UTF-8?q?test(codec):=20golden-vector=20parity=20t?= =?UTF-8?q?est=20=E2=80=94=20Rust=20+=20TS=20surfaces=20(T-P2-13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parity.test.ts: TS/JS surface, canonical + wire, both directions - parity.rs: Rust surface, canonical only (no wire encoder per B-v C3) - ci.yml: vector-parity job - 18/18 golden vectors pass parity on both surfaces - malformed vectors assert expected CodecError variant --- .github/workflows/ci.yml | 21 ++ packages/codec/Cargo.lock | 17 ++ packages/codec/Cargo.toml | 1 + packages/codec/tests/parity.rs | 356 ++++++++++++++++++++++++++++ packages/codec/tests/parity.test.ts | 188 +++++++++++++++ 5 files changed, 583 insertions(+) create mode 100644 packages/codec/tests/parity.rs create mode 100644 packages/codec/tests/parity.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d49ea5..7394175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,27 @@ jobs: 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: diff --git a/packages/codec/Cargo.lock b/packages/codec/Cargo.lock index 8678fdf..2792792 100644 --- a/packages/codec/Cargo.lock +++ b/packages/codec/Cargo.lock @@ -495,6 +495,21 @@ 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" @@ -692,8 +707,10 @@ dependencies = [ "js-sys", "phf", "proptest", + "ruint", "serde", "serde-wasm-bindgen", + "serde_json", "thiserror", "tiny-keccak", "wasm-bindgen", diff --git a/packages/codec/Cargo.toml b/packages/codec/Cargo.toml index eb8e04b..2fa1eb0 100644 --- a/packages/codec/Cargo.toml +++ b/packages/codec/Cargo.toml @@ -24,6 +24,7 @@ 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] 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) + }) + } +}) From bf487172bfbc36bc816eae9b7f82977b1996aa7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 22:59:01 +0000 Subject: [PATCH 35/35] chore(deps): bump actions/setup-node from 4 to 6 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7394175..e667e66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: { version: 10.24.0 } - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: { node-version: 24, cache: pnpm } - uses: actions-rust-lang/setup-rust-toolchain@v1 with: { rustflags: "" } @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: { version: 10.24.0 } - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: { node-version: 24, cache: pnpm } - uses: actions-rust-lang/setup-rust-toolchain@v1 with: { rustflags: "" } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 256e4a6..d58b17c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: { node-version: 24 } - run: | echo "Phase 1: release.yml plumbing reserved. Publish job lands Phase 3."