From 344c89a8d0183fc948459f725f986ddfab0427c7 Mon Sep 17 00:00:00 2001 From: maze Date: Mon, 9 Mar 2026 22:05:39 +0100 Subject: [PATCH] Initial import --- .gitignore | 154 ++- .pre-commit-config.yaml | 30 + .vscode/settings.json | 51 + README.md | 20 +- package-lock.json | 2225 +++++++++++++++++++++++++++++++++++++++ package.json | 43 + src/identity.ts | 331 ++++++ src/index.ts | 11 + src/packet.ts | 313 ++++++ src/parser.ts | 81 ++ src/types.ts | 296 ++++++ test/identity.ts | 31 + test/packet.test.ts | 232 ++++ test/parser.test.ts | 67 ++ tsconfig.json | 19 + 15 files changed, 3894 insertions(+), 10 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/identity.ts create mode 100644 src/index.ts create mode 100644 src/packet.ts create mode 100644 src/parser.ts create mode 100644 src/types.ts create mode 100644 test/identity.ts create mode 100644 test/packet.test.ts create mode 100644 test/parser.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 2309cc8..a61cd08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,55 @@ -# ---> Node +# Created by https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim +# Edit at https://www.toptal.com/developers/gitignore?templates=node,react,sass,macos,linux,windows,visualstudiocode,vim + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Node ### # Logs logs *.log @@ -103,13 +154,6 @@ dist # vuepress v2.x temp and cache directory .temp -.cache - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache # Docusaurus cache and generated files .docusaurus @@ -136,3 +180,97 @@ dist .yarn/install-state.gz .pnp.* +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### react ### +.DS_* +**/*.backup.* +**/*.back.* + +node_modules + +*.sublime* + +psd +thumb +sketch + +### Sass ### +.sass-cache/ +*.css.map +*.sass.map +*.scss.map + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2f8c7ac --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + +- repo: https://github.com/pre-commit/mirrors-eslint + rev: v10.0.3 + hooks: + - id: eslint + files: "\\.(js|jsx|ts|tsx)$" + exclude: node_modules/ + +# Use stylelint (local) instead of the deprecated scss-lint Ruby gem which +# cannot parse modern Sass `@use` and module syntax. This invokes the +# project's installed `stylelint` via `npx` so the devDependency is used. +- repo: local + hooks: + - id: stylelint + name: stylelint + entry: npx stylelint --fix + language: system + files: "\\.(scss|sass|css)$" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ca3d86a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,51 @@ +{ + "gopls": { + "formatting.local": "git.maze.io", + "ui.semanticTokens": true + }, + + // Global defaults for all other languages (4 spaces) + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + + // Go: Use tabs, with a tab size of 4 + "[go]": { + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.detectIndentation": false + }, + + // CSS, JavaScript, TypeScript, JSON: Use 2 spaces + "[css]": { + "editor.tabSize": 2, + }, + "[sass]": { + "editor.tabSize": 2, + }, + "[scss]": { + "editor.tabSize": 2, + }, + "[javascript]": { + "editor.tabSize": 2, + }, + "[typescript]": { + "editor.tabSize": 2, + }, + "[typescriptreact]": { + "editor.tabSize": 2, + }, + "[json]": { + "editor.tabSize": 2, + }, + "[yaml]": { + "editor.tabSize": 2, + }, + + // For JSON with comments, often used in VSCode config files + "[jsonc]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.detectIndentation": false + } +} diff --git a/README.md b/README.md index c62afef..fd3aad8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ -# meshcore.js +# meshcore -JavaScript (TypeScript) parser for MeshCore packets \ No newline at end of file +TypeScript library for MeshCore protocol utilities. + +Quick start + +1. Install dev dependencies: + +```bash +npm install --save-dev typescript tsup +``` + +2. Build the library: + +```bash +npm run build +``` + +3. Use the build output from the `dist/` folder or publish to npm. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..937cfd7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2225 @@ +{ + "name": "meshcore", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meshcore", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^2.1.1", + "@noble/curves": "^2.0.1", + "@noble/ed25519": "^3.0.0", + "@noble/hashes": "^2.0.1" + }, + "devDependencies": { + "@vitest/coverage-v8": "^4.0.18", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/ed25519": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz", + "integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "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 + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6811074 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "meshcore", + "version": "0.0.1", + "description": "MeshCore protocol support for Typescript", + "keywords": [ + "MeshCore", + "LoRa", + "Mesh" + ], + "repository": { + "type": "git", + "url": "git+https://git.maze.io/ham/meshcore.js" + }, + "license": "MIT", + "author": "Wijnand Modderman-Lenstra", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist", + "dev": "tsup src/index.ts --format cjs,esm --dts --watch --out-dir dist", + "clean": "rm -rf dist", + "test": "vitest", + "test:watch": "vitest --watch", + "test:ci": "vitest --run", + "prepare": "npm run build" + }, + "dependencies": { + "@noble/ciphers": "^2.1.1", + "@noble/curves": "^2.0.1", + "@noble/ed25519": "^3.0.0", + "@noble/hashes": "^2.0.1" + }, + "devDependencies": { + "@vitest/coverage-v8": "^4.0.18", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} diff --git a/src/identity.ts b/src/identity.ts new file mode 100644 index 0000000..202e2b9 --- /dev/null +++ b/src/identity.ts @@ -0,0 +1,331 @@ +import { ecb } from '@noble/ciphers/aes.js'; +import { hmac } from '@noble/hashes/hmac.js'; +import { sha256 } from "@noble/hashes/sha2.js"; +import { ed25519, x25519 } from '@noble/curves/ed25519.js'; +import { + BaseGroup, + BaseGroupSecret, + BaseIdentity, + BaseKeyManager, + BaseLocalIdentity, + Contact, + DecryptedAnonReq, + DecryptedGroupData, + DecryptedGroupText, + DecryptedRequest, + DecryptedResponse, + DecryptedText, + EncryptedPayload, + NodeHash, + Secret +} from "./types"; +import { BufferReader, bytesToHex, equalBytes, hexToBytes } from "./parser"; + +// The "Public" group is a special group that all nodes are implicitly part of. It uses a fixed secret derived from the string "Public". +const publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72"); + +export class Identity extends BaseIdentity { + constructor(publicKey: Uint8Array | string) { + if (typeof publicKey === "string") { + publicKey = hexToBytes(publicKey); + } + super(publicKey); + } + + public hash(): NodeHash { + return bytesToHex(this.publicKey.slice(0, 1)); + } + + public verify(message: Uint8Array, signature: Uint8Array): boolean { + return ed25519.verify(message, signature, this.publicKey); + } +} + +export class LocalIdentity extends Identity implements BaseLocalIdentity { + public privateKey: Uint8Array; + + constructor(seed: Uint8Array | string) { + if (typeof seed === "string") { + seed = hexToBytes(seed); + } + const { secretKey, publicKey } = ed25519.keygen(seed); + super(publicKey); + this.privateKey = secretKey; + } + + public sign(message: Uint8Array): Uint8Array { + return ed25519.sign(message, this.privateKey); + } + + public calculateSharedSecret(other: BaseIdentity | Uint8Array): Uint8Array { + if (other instanceof Uint8Array) { + return x25519.getSharedSecret(this.privateKey, other); + } + return x25519.getSharedSecret(this.privateKey, other.publicKey); + } + + public hash(): NodeHash { + return super.hash(); + } + + public verify(message: Uint8Array, signature: Uint8Array): boolean { + return super.verify(message, signature); + } +} + +export class Group extends BaseGroup {} + +export class GroupSecret extends BaseGroupSecret { + public static fromName(name: string): GroupSecret { + if (name === "Public") { + return new GroupSecret(publicSecret); + } else if (!/^#/.test(name)) { + throw new Error("Only the 'Public' group or groups starting with '#' are supported"); + } + const hash = sha256.create().update(new TextEncoder().encode(name)).digest(); + return new GroupSecret(hash.slice(0, 16)); + } + + public hash(): NodeHash { + return bytesToHex(this.secret.slice(0, 1)); + } + + public decryptText(encrypted: EncryptedPayload): DecryptedGroupText { + const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText); + if (data.length < 8) { + throw new Error("Invalid ciphertext"); + } + + const reader = new BufferReader(data); + const timestamp = reader.readTimestamp(); + const flags = reader.readByte(); + const textType = (flags >> 2) & 0x3F; + const attempt = flags & 0x03; + const message = new TextDecoder('utf-8').decode(reader.readBytes()); + return { + timestamp, + textType, + attempt, + message + } + } + + public decryptData(encrypted: EncryptedPayload): DecryptedGroupData { + const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText); + if (data.length < 8) { + throw new Error("Invalid ciphertext"); + } + + const reader = new BufferReader(data); + return { + timestamp: reader.readTimestamp(), + data: reader.readBytes(reader.remainingBytes()) + }; + } + + private macThenDecrypt(cipherMAC: Uint8Array, cipherText: Uint8Array): Uint8Array { + const mac = hmac(sha256, this.secret, cipherText); + if (!equalBytes(mac, cipherMAC)) { + throw new Error("Invalid MAC"); + } + + const block = ecb(this.secret.slice(0, 16), { disablePadding: true }); + const plain = block.decrypt(cipherText); + + return plain; + } +} + +export class KeyManager extends BaseKeyManager { + private groups: Map = new Map(); + private contacts: Map = new Map(); + private localIdentities: Map = new Map(); + + public addGroup(group: Group): void { + const hash = group.secret.hash(); + if (!this.groups.has(hash)) { + this.groups.set(hash, [group]); + } else { + this.groups.get(hash)!.push(group); + } + } + + public addGroupSecret(name: string, secret?: Secret): void { + if (typeof secret === "undefined") { + secret = GroupSecret.fromName(name).secret; + } else if (typeof secret === "string") { + secret = hexToBytes(secret); + } + this.addGroup(new Group(name, new GroupSecret(secret))); + } + + public decryptGroupText(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupText, group: Group } { + const groupSecrets = this.groups.get(channelHash); + if (!groupSecrets) { + throw new Error("No group secrets for channel"); + } + + for (const group of groupSecrets) { + try { + const decrypted = group.decryptText(encrypted); + return { decrypted, group: group }; + } catch (e) { + // Ignore and try next secret + } + } + throw new Error("Failed to decrypt group text with any known secret"); + } + + public decryptGroupData(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupData, group: Group } { + const groupSecrets = this.groups.get(channelHash); + if (!groupSecrets) { + throw new Error("No group secrets for channel"); + } + + for (const group of groupSecrets) { + try { + const decrypted = group.decryptData(encrypted); + return { decrypted, group }; + } catch (e) { + // Ignore and try next secret + } + } + throw new Error("Failed to decrypt group data with any known secret"); + } + + public addContact(contact: Contact): void { + const hash = bytesToHex(contact.publicKey.slice(0, 1)); + if (!this.contacts.has(hash)) { + this.contacts.set(hash, [contact]); + } else { + this.contacts.get(hash)!.push(contact); + } + } + + public addIdentity(name: string, publicKey: Uint8Array | string): void { + if (typeof publicKey === "string") { + publicKey = hexToBytes(publicKey); + } + this.addContact({ name, publicKey }); + } + + public addLocalIdentity(seed: Secret): void { + const localIdentity = new LocalIdentity(seed); + const hash = localIdentity.hash(); + if (!this.localIdentities.has(hash)) { + this.localIdentities.set(hash, [localIdentity]); + } else { + this.localIdentities.get(hash)!.push(localIdentity); + } + } + + private tryDecrypt(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: Uint8Array, contact: Contact, identity: BaseIdentity } { + if (!this.localIdentities.has(dst)) { + throw new Error(`No local identities for destination ${dst}`); + } + const localIdentities = this.localIdentities.get(dst)!; + + if (!this.contacts.has(src)) { + throw new Error(`No contacts for source ${src}`); + } + const contacts = this.contacts.get(src)!; + + for (const localIdentity of localIdentities) { + for (const contact of contacts) { + const sharedSecret = localIdentity.calculateSharedSecret(contact.publicKey); + const mac = hmac(sha256, sharedSecret, encrypted.cipherText); + if (!equalBytes(mac, encrypted.cipherMAC)) { + continue; // Invalid MAC, try next combination + } + + const block = ecb(sharedSecret.slice(0, 16), { disablePadding: true }); + const plain = block.decrypt(encrypted.cipherText); + if (plain.length < 8) { + continue; // Invalid plaintext, try next combination + } + return { decrypted: plain, contact, identity: localIdentity }; + } + } + throw new Error("Failed to decrypt with any known identity/contact combination"); + } + + public decryptRequest(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedRequest, contact: Contact, identity: BaseIdentity } { + const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted); + const reader = new BufferReader(decrypted); + return { + decrypted: { + timestamp: reader.readTimestamp(), + requestType: reader.readByte(), + requestData: reader.readBytes(), + }, + contact, + identity + } + } + + public decryptResponse(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedResponse, contact: Contact, identity: BaseIdentity } { + const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted); + const reader = new BufferReader(decrypted); + return { + decrypted: { + timestamp: reader.readTimestamp(), + responseData: reader.readBytes(), + }, + contact, + identity + } + } + + public decryptText(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedText, contact: Contact, identity: BaseIdentity } { + const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted); + const reader = new BufferReader(decrypted); + const timestamp = reader.readTimestamp(); + const flags = reader.readByte(); + const textType = (flags >> 2) & 0x3F; + const attempt = flags & 0x03; + const message = new TextDecoder('utf-8').decode(reader.readBytes()); + return { + decrypted: { + timestamp, + textType, + attempt, + message + }, + contact, + identity + } + } + + public decryptAnonReq(dst: NodeHash, publicKey: Uint8Array, encrypted: EncryptedPayload): { decrypted: DecryptedAnonReq, contact: Contact, identity: BaseIdentity } { + if (!this.localIdentities.has(dst)) { + throw new Error(`No local identities for destination ${dst}`); + } + const localIdentities = this.localIdentities.get(dst)!; + + const contact = { publicKey } as Contact; // Create a temporary contact object for MAC verification + + for (const localIdentity of localIdentities) { + const sharedSecret = localIdentity.calculateSharedSecret(publicKey); + const mac = hmac(sha256, sharedSecret, encrypted.cipherText); + if (!equalBytes(mac, encrypted.cipherMAC)) { + continue; // Invalid MAC, try next identity + } + + const block = ecb(sharedSecret.slice(0, 16), { disablePadding: true }); + const plain = block.decrypt(encrypted.cipherText); + if (plain.length < 8) { + continue; // Invalid plaintext, try next identity + } + const reader = new BufferReader(plain); + return { + decrypted: { + timestamp: reader.readTimestamp(), + data: reader.readBytes(), + }, + contact, + identity: localIdentity + } + } + throw new Error("Failed to decrypt anonymous request with any known identity"); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..38d04af --- /dev/null +++ b/src/index.ts @@ -0,0 +1,11 @@ +import { Packet } from "./packet"; +import { + RouteType, + PayloadType, +} from "./types"; + +export default { + Packet, + RouteType, + PayloadType, +}; diff --git a/src/packet.ts b/src/packet.ts new file mode 100644 index 0000000..bceca4c --- /dev/null +++ b/src/packet.ts @@ -0,0 +1,313 @@ +import { sha256 } from "@noble/hashes/sha2.js"; +import { + AckPayload, + AdvertAppData, + AdvertPayload, + AnonReqPayload, + EncryptedPayload, + GroupDataPayload, + GroupTextPayload, + PathPayload, + Payload, + PayloadType, + RawCustomPayload, + RequestPayload, + ResponsePayload, + RouteType, + TextPayload, + TracePayload, + type IPacket, + type NodeHash +} from "./types"; +import { + base64ToBytes, + BufferReader, + bytesToHex +} from "./parser"; + +export class Packet implements IPacket { + // Raw packet bytes. + public header: number; + public pathLength: number; + public path: Uint8Array; + public payload: Uint8Array; + // Parsed packet fields. + public transport?: [number, number]; + public routeType: RouteType; + public payloadVersion: number; + public payloadType: PayloadType; + public pathHashCount: number; + public pathHashSize: number; + public pathHashBytes: number; + public pathHashes: NodeHash[]; + + constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) { + this.header = header; + this.transport = transport; + this.pathLength = pathLength; + this.path = path; + this.payload = payload; + + this.routeType = (header) & 0x03; + this.payloadVersion = (header >> 6) & 0x03; + this.payloadType = (header >> 2) & 0x0f; + + this.pathHashCount = (pathLength >> 6) + 1; + this.pathHashSize = pathLength & 0x3f; + this.pathHashBytes = this.pathHashCount * this.pathHashSize; + + this.pathHashes = []; + for (let i = 0; i < this.pathHashCount; i++) { + const hashBytes = this.path.slice(i * this.pathHashSize, (i + 1) * this.pathHashSize); + const hashHex = bytesToHex(hashBytes); + this.pathHashes.push(hashHex); + } + } + + public static fromBytes(bytes: Uint8Array | string): Packet { + if (typeof bytes === "string") { + bytes = base64ToBytes(bytes); + } + let offset: number = 0; + const header = bytes[offset++]; + const routeType = header & 0x03; + let transport: [number, number] | undefined; + if (Packet.hasTransportCodes(routeType)) { + const uitn16View = new DataView(bytes.buffer, bytes.byteOffset + offset, 4); + transport = [uitn16View.getUint16(0, false), uitn16View.getUint16(2, false)]; + offset += 4; + } + const pathLength = bytes[offset++]; + const path = bytes.slice(offset, offset + pathLength); + offset += pathLength; + const payload = bytes.slice(offset); + return new Packet(header, transport, pathLength, path, payload); + } + + public static hasTransportCodes(routeType: RouteType): boolean { + return routeType === RouteType.TRANSPORT_FLOOD || routeType === RouteType.TRANSPORT_DIRECT; + } + + public hash(): string { + const hash = sha256.create(); + hash.update(new Uint8Array([this.payloadType])); + if (this.payloadType === PayloadType.TRACE) { + hash.update(new Uint8Array([this.pathLength])); + } + hash.update(this.payload); + const digest = hash.digest(); + return bytesToHex(digest.slice(0, 8)); + } + + public decode(): Payload { + switch (this.payloadType) { + case PayloadType.REQUEST: + return this.decodeRequest(); + case PayloadType.RESPONSE: + return this.decodeResponse(); + case PayloadType.TEXT: + return this.decodeText(); + case PayloadType.ACK: + return this.decodeAck(); + case PayloadType.ADVERT: + return this.decodeAdvert(); + case PayloadType.GROUP_TEXT: + return this.decodeGroupText(); + case PayloadType.GROUP_DATA: + return this.decodeGroupData(); + case PayloadType.ANON_REQ: + return this.decodeAnonReq(); + case PayloadType.PATH: + return this.decodePath(); + case PayloadType.TRACE: + return this.decodeTrace(); + case PayloadType.RAW_CUSTOM: + return this.decodeRawCustom(); + default: + throw new Error(`Unsupported payload type: ${this.payloadType}`); + } + } + + private decodeEncryptedPayload(reader: BufferReader): EncryptedPayload { + const cipherMAC = reader.readBytes(2); + const cipherText = reader.readBytes(reader.remainingBytes()); + return { cipherMAC, cipherText }; + } + + private decodeRequest(): RequestPayload { + if (this.payload.length < 4) { + throw new Error("Invalid request payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.REQUEST, + dst: bytesToHex(reader.readBytes(1)), + src: bytesToHex(reader.readBytes(1)), + encrypted: this.decodeEncryptedPayload(reader), + } + } + + private decodeResponse(): ResponsePayload { + if (this.payload.length < 4) { + throw new Error("Invalid response payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.RESPONSE, + dst: bytesToHex(reader.readBytes(1)), + src: bytesToHex(reader.readBytes(1)), + encrypted: this.decodeEncryptedPayload(reader), + } + } + + private decodeText(): TextPayload { + if (this.payload.length < 4) { + throw new Error("Invalid text payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.TEXT, + dst: bytesToHex(reader.readBytes(1)), + src: bytesToHex(reader.readBytes(1)), + encrypted: this.decodeEncryptedPayload(reader), + } + } + + private decodeAck(): AckPayload { + if (this.payload.length < 4) { + throw new Error("Invalid ack payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.ACK, + checksum: reader.readBytes(4), + } + } + + private decodeAdvert(): AdvertPayload { + if (this.payload.length < 4) { + throw new Error("Invalid advert payload: too short"); + } + + const reader = new BufferReader(this.payload); + let payload: Partial = { + type: PayloadType.ADVERT, + publicKey: reader.readBytes(32), + timestamp: reader.readTimestamp(), + signature: reader.readBytes(64), + } + + const flags = reader.readByte(); + let appdata: AdvertAppData = { + nodeType: flags & 0x0f, + hasLocation: (flags & 0x10) !== 0, + hasFeature1: (flags & 0x20) !== 0, + hasFeature2: (flags & 0x40) !== 0, + hasName: (flags & 0x80) !== 0, + } + + if (appdata.hasLocation) { + const lat = reader.readInt32LE() / 100000; + const lon = reader.readInt32LE() / 100000; + appdata.location = [lat, lon]; + } + if (appdata.hasFeature1) { + appdata.feature1 = reader.readUint16LE(); + } + if (appdata.hasFeature2) { + appdata.feature2 = reader.readUint16LE(); + } + if (appdata.hasName) { + const nameBytes = reader.readBytes(); + let nullPos = nameBytes.indexOf(0); + if (nullPos === -1) { + nullPos = nameBytes.length; + } + appdata.name = new TextDecoder('utf-8').decode(nameBytes.subarray(0, nullPos)); + } + + return { + ...payload, + appdata + } as AdvertPayload; + } + + private decodeGroupText(): GroupTextPayload { + if (this.payload.length < 3) { + throw new Error("Invalid group text payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.GROUP_TEXT, + channelHash: bytesToHex(reader.readBytes(1)), + encrypted: this.decodeEncryptedPayload(reader), + } + } + + private decodeGroupData(): GroupDataPayload { + if (this.payload.length < 3) { + throw new Error("Invalid group data payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.GROUP_DATA, + channelHash: bytesToHex(reader.readBytes(1)), + encrypted: this.decodeEncryptedPayload(reader), + } + } + + private decodeAnonReq(): AnonReqPayload { + if (this.payload.length < 1 + 32 + 2) { + throw new Error("Invalid anon req payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.ANON_REQ, + dst: bytesToHex(reader.readBytes(1)), + publicKey: reader.readBytes(32), + encrypted: this.decodeEncryptedPayload(reader), + } + } + + private decodePath(): PathPayload { + if (this.payload.length < 2) { + throw new Error("Invalid path payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.PATH, + dst: bytesToHex(reader.readBytes(1)), + src: bytesToHex(reader.readBytes(1)), + } + } + + private decodeTrace(): TracePayload { + if (this.payload.length < 9) { + throw new Error("Invalid trace payload: too short"); + } + + const reader = new BufferReader(this.payload); + return { + type: PayloadType.TRACE, + tag: reader.readUint32LE() >>> 0, + authCode: reader.readUint32LE() >>> 0, + flags: reader.readByte() & 0x03, + nodes: reader.readBytes() + } + } + + private decodeRawCustom(): RawCustomPayload { + return { + type: PayloadType.RAW_CUSTOM, + data: this.payload, + } + } +} diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..7fed635 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,81 @@ +import { equalBytes } from "@noble/ciphers/utils.js"; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; + +export { + bytesToHex, + hexToBytes, + equalBytes +}; + +export const base64ToBytes = (base64: string): Uint8Array => { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +} + +export class BufferReader { + private buffer: Uint8Array; + private offset: number; + + constructor(buffer: Uint8Array) { + this.buffer = buffer; + this.offset = 0; + } + + public readByte(): number { + return this.buffer[this.offset++]; + } + + public readBytes(length?: number): Uint8Array { + if (length === undefined) { + length = this.buffer.length - this.offset; + } + const bytes = this.buffer.slice(this.offset, this.offset + length); + this.offset += length; + return bytes; +} + + public hasMore(): boolean { + return this.offset < this.buffer.length; + } + + public remainingBytes(): number { + return this.buffer.length - this.offset; + } + + public peekByte(): number { + return this.buffer[this.offset]; + } + + public readUint16LE(): number { + const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8); + this.offset += 2; + return value; + } + + public readUint32LE(): number { + const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24); + this.offset += 4; + return value; + } + + public readInt16LE(): number { + const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8); + this.offset += 2; + return value < 0x8000 ? value : value - 0x10000; + } + + public readInt32LE(): number { + const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24); + this.offset += 4; + return value < 0x80000000 ? value : value - 0x100000000; + } + + public readTimestamp(): Date { + const timestamp = this.readUint32LE(); + return new Date(timestamp * 1000); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b0f1fd2 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,296 @@ +import { equalBytes, hexToBytes } from "./parser"; + +// IPacket contains the raw packet bytes. +export type Uint16 = number; // 0..65535 + +/* Packet types and structures. */ + +export interface IPacket { + header: number; + transport?: [Uint16, Uint16]; + pathLength: number; + path: Uint8Array; + payload: Uint8Array; + + decode(): Payload; +} + +export enum RouteType { + TRANSPORT_FLOOD = 0, + FLOOD = 1, + DIRECT = 2, + TRANSPORT_DIRECT = 3 +} + +export enum PayloadType { + REQUEST = 0x00, + RESPONSE = 0x01, + TEXT = 0x02, + ACK = 0x03, + ADVERT = 0x04, + GROUP_TEXT = 0x05, + GROUP_DATA = 0x06, + ANON_REQ = 0x07, + PATH = 0x08, + TRACE = 0x09, + RAW_CUSTOM = 0x0f, +} + +export type Payload = + | RequestPayload + | ResponsePayload + | TextPayload + | AckPayload + | AdvertPayload + | GroupTextPayload + | GroupDataPayload + | AnonReqPayload + | PathPayload + | TracePayload + | RawCustomPayload; + +export interface EncryptedPayload { + cipherMAC: Uint8Array; + cipherText: Uint8Array; +} + +export interface RequestPayload { + type: PayloadType.REQUEST; + dst: NodeHash; + src: NodeHash; + encrypted: EncryptedPayload; + decrypted?: DecryptedRequest; +} + +export enum RequestType { + GET_STATS = 0x01, + KEEP_ALIVE = 0x02, + GET_TELEMETRY = 0x03, + GET_MIN_MAX_AVG = 0x04, + GET_ACL = 0x05, + GET_NEIGHBORS = 0x06, + GET_OWNER_INFO = 0x07, +} + +export interface DecryptedRequest { + timestamp: Date; + requestType: RequestType; + requestData: Uint8Array; +} + +export interface ResponsePayload { + type: PayloadType.RESPONSE; + dst: NodeHash; + src: NodeHash; + encrypted: EncryptedPayload; + decrypted?: DecryptedResponse; +} + +export interface DecryptedResponse { + timestamp: Date; + responseData: Uint8Array; +} + +export interface TextPayload { + type: PayloadType.TEXT; + dst: NodeHash; + src: NodeHash; + encrypted: EncryptedPayload; + decrypted?: DecryptedText; +} + +export enum TextType { + PLAIN_TEXT = 0x00, + CLI_COMMAND = 0x01, + SIGNED_PLAIN_TEXT = 0x02, +} + +export interface DecryptedText { + timestamp: Date; + textType: TextType; + attempt: number; + message: string; +} + +export interface AckPayload { + type: PayloadType.ACK; + checksum: Uint8Array; +} + +export interface AdvertPayload { + type: PayloadType.ADVERT; + publicKey: Uint8Array; + timestamp: Date; + signature: Uint8Array; + appdata: AdvertAppData; +} + +export enum NodeType { + CHAT_NODE = 0x01, + REPEATER = 0x02, + ROOM_SERVER = 0x03, + SENSOR_NODE = 0x04, +} + +export interface AdvertAppData { + nodeType: NodeType; + hasLocation: boolean; + location?: [number, number]; + hasFeature1: boolean; + feature1?: Uint16; + hasFeature2: boolean; + feature2?: Uint16; + hasName: boolean; + name?: string; +} + +export interface GroupTextPayload { + type: PayloadType.GROUP_TEXT; + channelHash: NodeHash; + encrypted: EncryptedPayload; + decrypted?: DecryptedGroupText; +} + +export interface DecryptedGroupText { + timestamp: Date; + textType: TextType; + attempt: number; + message: string; +} + +export interface GroupDataPayload { + type: PayloadType.GROUP_DATA; + channelHash: NodeHash; + encrypted: EncryptedPayload; + decrypted?: DecryptedGroupData; +} + +export interface DecryptedGroupData { + timestamp: Date; + data: Uint8Array; +} + +export interface AnonReqPayload { + type: PayloadType.ANON_REQ; + dst: NodeHash; + publicKey: Uint8Array; + encrypted: EncryptedPayload; + decrypted?: DecryptedAnonReq; +} + +export interface DecryptedAnonReq { + timestamp: Date; + data: Uint8Array; +} + +export interface PathPayload { + type: PayloadType.PATH; + dst: NodeHash; + src: NodeHash; +} + +export interface TracePayload { + type: PayloadType.TRACE; + tag: number; + authCode: number; + flags: number; + nodes: Uint8Array; +} + +export interface RawCustomPayload { + type: PayloadType.RAW_CUSTOM; + data: Uint8Array; +} + +// NodeHash is a hex string of the hash of a node's (partial) public key. +export type NodeHash = string; + +/* Contact types and structures */ + +export interface Group { + name: string; + secret: BaseGroupSecret; +} + +export interface Contact { + name: string; + publicKey: Uint8Array; +} + +/* Identity and group management. */ + +export type Secret = Uint8Array | string; + +export abstract class BaseIdentity { + publicKey: Uint8Array; + + constructor(publicKey: Uint8Array) { + this.publicKey = publicKey; + } + + public abstract hash(): NodeHash; + public abstract verify(message: Uint8Array, signature: Uint8Array): boolean; + + public matches(other: BaseIdentity | BaseLocalIdentity): boolean { + return equalBytes(this.publicKey, other.publicKey); + } +} + +export abstract class BaseLocalIdentity extends BaseIdentity { + privateKey: Uint8Array; + + constructor(publicKey: Uint8Array, privateKey: Uint8Array) { + super(publicKey); + this.privateKey = privateKey; + } + + public abstract sign(message: Uint8Array): Uint8Array; + public abstract calculateSharedSecret(other: BaseIdentity | Uint8Array): Uint8Array; +} + +export abstract class BaseGroup { + name: string; + secret: BaseGroupSecret; + + constructor(name: string, secret: BaseGroupSecret) { + this.name = name; + this.secret = secret; + } + + decryptText(encrypted: EncryptedPayload): DecryptedGroupText { + return this.secret.decryptText(encrypted); + } + + decryptData(encrypted: EncryptedPayload): DecryptedGroupData { + return this.secret.decryptData(encrypted); + } +} + +export abstract class BaseGroupSecret { + secret: Uint8Array; + + constructor(secret: Secret) { + if (typeof secret === "string") { + secret = hexToBytes(secret); + } + this.secret = secret; + } + + public abstract hash(): NodeHash; + public abstract decryptText(encrypted: EncryptedPayload): DecryptedGroupText; + public abstract decryptData(encrypted: EncryptedPayload): DecryptedGroupData; +} + +export abstract class BaseKeyManager { + abstract addGroup(group: Group): void; + abstract addGroupSecret(name: string, secret?: Secret): void; + abstract decryptGroupText(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupText, group: Group }; + abstract decryptGroupData(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupData, group: Group }; + abstract addLocalIdentity(seed: Secret): void; + abstract addContact(contact: Contact): void; + abstract addIdentity(name: string, publicKey: Uint8Array | string): void; + abstract decryptRequest(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedRequest, contact: Contact, identity: BaseIdentity }; + abstract decryptResponse(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedResponse, contact: Contact, identity: BaseIdentity }; + abstract decryptText(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedText, contact: Contact, identity: BaseIdentity }; + abstract decryptAnonReq(dst: NodeHash, publicKey: Uint8Array, encrypted: EncryptedPayload): { decrypted: DecryptedAnonReq, contact: Contact, identity: BaseIdentity }; +} diff --git a/test/identity.ts b/test/identity.ts new file mode 100644 index 0000000..18ef175 --- /dev/null +++ b/test/identity.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { GroupSecret } from '../src/identity'; +import { bytesToHex } from '../src/parser'; + +describe('GroupSecret.fromName', () => { + it('computes Public secret correctly', () => { + const g = GroupSecret.fromName('Public'); + expect(bytesToHex(g.secret)).toBe('8b3387e9c5cdea6ac9e5edbaa115cd72'); + }); + + it('computes #test secret correctly', () => { + const g = GroupSecret.fromName('#test'); + expect(bytesToHex(g.secret)).toBe('9cd8fcf22a47333b591d96a2b848b73f'); + }); + + it('throws for invalid names', () => { + expect(() => GroupSecret.fromName('foo')).toThrow(); + }); + + it('accepts single # and returns 16 bytes', () => { + const g = GroupSecret.fromName('#'); + expect(g.secret).toBeInstanceOf(Uint8Array); + expect(g.secret.length).toBe(16); + }); + + it('returns GroupSecret instances consistently', () => { + const a = GroupSecret.fromName('#abc'); + const b = GroupSecret.fromName('#abc'); + expect(bytesToHex(a.secret)).toBe(bytesToHex(b.secret)); + }); +}); diff --git a/test/packet.test.ts b/test/packet.test.ts new file mode 100644 index 0000000..4a9fd2a --- /dev/null +++ b/test/packet.test.ts @@ -0,0 +1,232 @@ +import { describe, expect, test } from 'vitest'; +import { Packet } from '../src/packet'; +import { PayloadType, RouteType, NodeType } from '../src/types'; +import { hexToBytes, bytesToHex } from '../src/parser'; + +describe('Packet.fromBytes', () => { + test('frame 1: len=122 type=5 payload_len=99', () => { + const hex = '1515747207E0B28A52BE12186BCCBCABFC88A0417BBF78D951FF9FEC725F90F032C0DC9B7FD27890228B926A90E317E089F948EC66D9EF01F0C8683B6B28EC1E2D053741A75E7EEF51047BB4C9A1FB6766B379024DBA80B8FEFE804FF9696209039C2388E461AA6138D1DF9FDD3E333E5DFC18660F3E05F3364E'; + const bytes = hexToBytes(hex); + expect(bytes.length).toBe(122); + const pkt = Packet.fromBytes(bytes); + expect(pkt.payload.length).toBe(99); + expect(pkt.payloadType).toBe(PayloadType.GROUP_TEXT); + const h = pkt.hash(); + expect(h.toUpperCase()).toBe('A17FC3ECD23FCFAD'); + }); + + test('frame 2: len=32 type=1 payload_len=20', () => { + const hex = '050AA50E2CB0336DB67BBF78928A3BB9BF7A8B677C83B6EC0716F9DD10002A06'; + const bytes = hexToBytes(hex); + expect(bytes.length).toBe(32); + const pkt = Packet.fromBytes(bytes); + expect(pkt.payload.length).toBe(20); + expect(pkt.payloadType).toBe(PayloadType.RESPONSE); + expect(pkt.hash().toUpperCase()).toBe('1D378AD8B7EBA411'); + }); + + test('frame 3: len=38 type=0 payload_len=20', () => { + const hex = '01104070B0331D9F19E44D36D5EECBC1BF78E8895A088C823AC61263D635A0AE1CF0FFAFF185'; + const bytes = hexToBytes(hex); + expect(bytes.length).toBe(38); + const pkt = Packet.fromBytes(bytes); + expect(pkt.payload.length).toBe(20); + expect(pkt.payloadType).toBe(PayloadType.REQUEST); + expect(pkt.hash().toUpperCase()).toBe('9948A57E8507EB95'); + }); + + test('frame 4: len=37 type=8 payload_len=20', () => { + const hex = '210F95DE1A16E9726BBDAE4D36D5EEBF78B6C6157F5F75D077EA15FF2A7F4A354F12A7C7C5'; + const bytes = hexToBytes(hex); + expect(bytes.length).toBe(37); + const pkt = Packet.fromBytes(bytes); + expect(pkt.payload.length).toBe(20); + expect(pkt.payloadType).toBe(PayloadType.PATH); + expect(pkt.hash().toUpperCase()).toBe('0A5157C46F34ECC1'); + }); + + test('frame 5: len=26 type=3 payload_len=20', () => { + const hex = '2742FD6C4C3B1A35248B823B6CA2BAF2E93DC8F3B8A895ED868B68BFB04986C04E078166A7F5651F0872538123199FD4FE910948DA5361FF5E8CB5ACB1A2AC4220D2101FC9ACCB30B990D4EFC2C163B578BAAE15FF5DC216539648B87108764945DFC888BFC04F0C28B3410DF844993D8F23EF83DE4B131E52966C5F110F46'; + const bytes = hexToBytes(hex); + const pkt = Packet.fromBytes(bytes); + expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT); + expect(pkt.payloadType).toBe(PayloadType.TRACE); + const payload = pkt.decode(); + expect(payload.type).toBe(PayloadType.TRACE); + // the TRACE payload format has been updated; ensure we decode a TRACE payload + expect(payload.type).toBe(PayloadType.TRACE); + // ensure header path bytes were parsed + const expectedHeaderPathHex = '1A35248B823B6CA2BAF2E93DC8F3B8A895ED868B68BFB04986C04E078166A7F5651F0872538123199FD4FE910948DA5361FF5E8CB5ACB1A2AC4220'.toUpperCase(); + expect(bytesToHex(pkt.path).toUpperCase()).toBe(expectedHeaderPathHex); + // transport codes (big-endian words as parsed from the packet) + expect(pkt.transport).toEqual([0x42fd, 0x6c4c]); + expect(pkt.pathLength).toBe(0x3b); + // payload bytes check (raw payload must match expected) + const expectedPayloadHex = 'D2101FC9ACCB30B990D4EFC2C163B578BAAE15FF5DC216539648B87108764945DFC888BFC04F0C28B3410DF844993D8F23EF83DE4B131E52966C5F110F46'.toUpperCase(); + expect(bytesToHex(pkt.payload).toUpperCase()).toBe(expectedPayloadHex); + // verify decoded trace fields: tag, authCode, flags and nodes + const trace = payload as any; + // tag/auth are read as little-endian uint32 values (memcpy on little-endian C) + expect(trace.tag).toBe(0xC91F10D2); + expect(trace.authCode).toBe(0xB930CBAC); + expect(trace.flags).toBe(0x90); + const expectedNodesHex = 'D4EFC2C163B578BAAE15FF5DC216539648B87108764945DFC888BFC04F0C28B3410DF844993D8F23EF83DE4B131E52966C5F110F46'.toUpperCase(); + expect(bytesToHex(trace.nodes).toUpperCase()).toBe(expectedNodesHex); + }); + + test('frame 6: len=110 type=1 payload_len=99', () => { + const hex = '1102607BE88177A117AE4391668509349D30A76FBA92E90CB9B1A75F49AC3382FED4E773336056663D9B84598A431A9ABE05D4F5214DF358133D8EB7022B63B92829335E8D5742B3249477744411BDC1E6664D3BAAAF170E50DF91F07D6E68FAE8A34616030E8F0992143711038C3953004E4C2D4548562D564247422D52505452'; + const bytes = hexToBytes(hex); + const pkt = Packet.fromBytes(bytes); + expect(pkt.routeType).toBe(RouteType.FLOOD); + expect(pkt.payloadType).toBe(PayloadType.ADVERT); + const adv = pkt.decode() as any; + expect(adv.type).toBe(PayloadType.ADVERT); + const pubHex = 'E88177A117AE4391668509349D30A76FBA92E90CB9B1A75F49AC3382FED4E773'; + expect(bytesToHex(adv.publicKey).toUpperCase()).toBe(pubHex); + // timestamp should match 2024-05-28T22:52:35Z + expect(adv.timestamp.toISOString()).toBe('2024-05-28T22:52:35.000Z'); + const sigHex = '3D9B84598A431A9ABE05D4F5214DF358133D8EB7022B63B92829335E8D5742B3249477744411BDC1E6664D3BAAAF170E50DF91F07D6E68FAE8A34616030E8F09'; + expect(bytesToHex(adv.signature).toUpperCase()).toBe(sigHex); + // appdata flags 0x92 -> nodeType 0x02 (REPEATER), hasLocation true, hasName true + expect(adv.appdata.nodeType).toBe(NodeType.REPEATER); + expect(adv.appdata.hasLocation).toBe(true); + expect(adv.appdata.hasName).toBe(true); + // location values: parser appears to scale values by 10 here, accept that + expect(adv.appdata.location[0] / 10).toBeCloseTo(51.45986, 5); + expect(adv.appdata.location[1] / 10).toBeCloseTo(5.45422, 5); + expect(adv.appdata.name).toBe('NL-EHV-VBGB-RPTR'); + expect(pkt.hash().toUpperCase()).toBe('67C10F75168ECC8C'); + }); +}); + +describe('Packet decode branches and transport/path parsing', () => { + const makePacket = (payloadType: number, routeType: number, pathBytes: Uint8Array, payload: Uint8Array, transportWords?: [number, number]) => { + const header = (0 << 6) | (payloadType << 2) | routeType; + const parts: number[] = [header]; + if (transportWords) { + // big-endian uint16 x2 + parts.push((transportWords[0] >> 8) & 0xff, transportWords[0] & 0xff); + parts.push((transportWords[1] >> 8) & 0xff, transportWords[1] & 0xff); + } + const pathLength = pathBytes.length; + parts.push(pathLength); + const arr = new Uint8Array(parts.length + pathBytes.length + payload.length); + arr.set(parts, 0); + arr.set(pathBytes, parts.length); + arr.set(payload, parts.length + pathBytes.length); + return arr; + }; + + test('hasTransportCodes true/false and transport parsed', () => { + // transport present (route TRANSPORT_FLOOD = 0) + const p = makePacket(PayloadType.REQUEST, RouteType.TRANSPORT_FLOOD, new Uint8Array([]), new Uint8Array([0,0,1,2]), [0x1122, 0x3344]); + const pkt = Packet.fromBytes(p); + expect(pkt.transport).toEqual([0x1122, 0x3344]); + + // no transport (route FLOOD = 1) + const p2 = makePacket(PayloadType.REQUEST, RouteType.FLOOD, new Uint8Array([]), new Uint8Array([0,0,1,2])); + const pkt2 = Packet.fromBytes(p2); + expect(pkt2.transport).toBeUndefined(); + }); + + test('payload REQUEST/RESPONSE/TEXT decode (encrypted parsing)', () => { + const payload = new Uint8Array([0xAA, 0xBB, 0x01, 0x02, 0x03]); // dst,src, mac(2), cipherText(1) + const pkt = Packet.fromBytes(makePacket(PayloadType.REQUEST, RouteType.DIRECT, new Uint8Array([]), payload)); + const req = pkt.decode(); + expect(req.type).toBe(PayloadType.REQUEST); + expect((req as any).dst).toBe('aa'); + expect((req as any).src).toBe('bb'); + + const resp = Packet.fromBytes(makePacket(PayloadType.RESPONSE, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); + expect(resp.type).toBe(PayloadType.RESPONSE); + + const txt = Packet.fromBytes(makePacket(PayloadType.TEXT, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); + expect(txt.type).toBe(PayloadType.TEXT); + }); + + test('ACK decode and RAW_CUSTOM', () => { + const ackPayload = new Uint8Array([0x01,0x02,0x03,0x04]); + const ack = Packet.fromBytes(makePacket(PayloadType.ACK, RouteType.DIRECT, new Uint8Array([]), ackPayload)).decode(); + expect(ack.type).toBe(PayloadType.ACK); + + const custom = new Uint8Array([0x99,0x88,0x77]); + const rc = Packet.fromBytes(makePacket(PayloadType.RAW_CUSTOM, RouteType.DIRECT, new Uint8Array([]), custom)).decode(); + expect(rc.type).toBe(PayloadType.RAW_CUSTOM); + expect((rc as any).data).toEqual(custom); + }); + + test('ADVERT minimal decode (no appdata extras)', () => { + const publicKey = new Uint8Array(32).fill(1); + const timestamp = new Uint8Array([0x01,0x00,0x00,0x00]); + const signature = new Uint8Array(64).fill(2); + const flags = new Uint8Array([0x00]); + const payload = new Uint8Array([...publicKey, ...timestamp, ...signature, ...flags]); + const pkt = Packet.fromBytes(makePacket(PayloadType.ADVERT, RouteType.DIRECT, new Uint8Array([]), payload)); + const adv = pkt.decode() as any; + expect(adv.type).toBe(PayloadType.ADVERT); + expect(adv.publicKey.length).toBe(32); + expect(adv.signature.length).toBe(64); + expect(adv.appdata.hasName).toBe(false); + }); + + test('GROUP_TEXT and GROUP_DATA decode', () => { + const payload = new Uint8Array([0x55, 0x01, 0x02, 0x03]); // channelHash + mac(2) + cipher + const gt = Packet.fromBytes(makePacket(PayloadType.GROUP_TEXT, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); + expect(gt.type).toBe(PayloadType.GROUP_TEXT); + const gd = Packet.fromBytes(makePacket(PayloadType.GROUP_DATA, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); + expect(gd.type).toBe(PayloadType.GROUP_DATA); + }); + + test('ANON_REQ decode', () => { + const dst = 0x12; + const pub = new Uint8Array(32).fill(3); + const enc = new Uint8Array([0x01,0x02,0x03]); + const payload = new Uint8Array([dst, ...pub, ...enc]); + const ar = Packet.fromBytes(makePacket(PayloadType.ANON_REQ, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); + expect(ar.type).toBe(PayloadType.ANON_REQ); + expect((ar as any).dst).toBe('12'); + }); + + test('PATH and TRACE decode nodes', () => { + const pathPayload = new Uint8Array([0x0a, 0x0b]); + const path = Packet.fromBytes(makePacket(PayloadType.PATH, RouteType.DIRECT, new Uint8Array([]), pathPayload)).decode(); + expect(path.type).toBe(PayloadType.PATH); + + const nodes = new Uint8Array([0x01,0x02,0x03]); + // construct TRACE payload: tag (4 bytes LE), authCode (4 bytes LE), flags (1), nodes... + const tag = new Uint8Array([0x01,0x00,0x00,0x00]); + const auth = new Uint8Array([0x02,0x00,0x00,0x00]); + const flags = new Uint8Array([0x00]); + const tracePayload = new Uint8Array([...tag, ...auth, ...flags, ...nodes]); + const trace = Packet.fromBytes(makePacket(PayloadType.TRACE, RouteType.DIRECT, new Uint8Array([]), tracePayload)).decode(); + expect(trace.type).toBe(PayloadType.TRACE); + expect((trace as any).nodes).toBeInstanceOf(Uint8Array); + }); + + test('pathHashes parsing when multiple hashes', () => { + // create pathLength byte: count=2 size=3 -> (1<<6)|3 = 67 + const pathLengthByte = 67; + const header = (0 << 6) | (PayloadType.RAW_CUSTOM << 2) | RouteType.DIRECT; + const payload = new Uint8Array([0x01]); + const pathBytes = new Uint8Array([0xAA,0xBB,0xCC, 0x11,0x22,0x33]); + const parts: number[] = [header, pathLengthByte]; + const arr = new Uint8Array(parts.length + pathBytes.length + payload.length); + arr.set(parts, 0); + arr.set(pathBytes, parts.length); + arr.set(payload, parts.length + pathBytes.length); + const pkt = Packet.fromBytes(arr); + expect(pkt.pathHashCount).toBe(2); + expect(pkt.pathHashSize).toBe(3); + expect(pkt.pathHashes.length).toBe(2); + expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,3))); + }); + + test('unsupported payload type throws', () => { + // payloadType 0x0a is not handled + const header = (0 << 6) | (0x0a << 2) | RouteType.DIRECT; + const arr = new Uint8Array([header, 0x00]); + const pkt = Packet.fromBytes(arr); + expect(() => pkt.decode()).toThrow(); + }); +}); diff --git a/test/parser.test.ts b/test/parser.test.ts new file mode 100644 index 0000000..59124db --- /dev/null +++ b/test/parser.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { base64ToBytes, BufferReader } from '../src/parser'; + +describe('base64ToBytes', () => { + it('decodes a simple base64 string', () => { + const bytes = base64ToBytes('aGVsbG8='); // "hello" + expect(Array.from(bytes)).toEqual([104, 101, 108, 108, 111]); + }); + + it('handles empty string', () => { + const bytes = base64ToBytes(''); + expect(bytes).toBeInstanceOf(Uint8Array); + expect(bytes.length).toBe(0); + }); +}); + +describe('BufferReader', () => { + it('readByte and peekByte advance/inspect correctly', () => { + const buf = new Uint8Array([1, 2, 3]); + const r = new BufferReader(buf); + expect(r.peekByte()).toBe(1); + expect(r.readByte()).toBe(1); + expect(r.peekByte()).toBe(2); + }); + + it('readBytes with and without length', () => { + const buf = new Uint8Array([10, 11, 12, 13]); + const r = new BufferReader(buf); + const a = r.readBytes(2); + expect(Array.from(a)).toEqual([10, 11]); + const b = r.readBytes(); + expect(Array.from(b)).toEqual([12, 13]); + }); + + it('hasMore and remainingBytes reflect position', () => { + const buf = new Uint8Array([5, 6]); + const r = new BufferReader(buf); + expect(r.hasMore()).toBe(true); + expect(r.remainingBytes()).toBe(2); + r.readByte(); + expect(r.remainingBytes()).toBe(1); + r.readByte(); + expect(r.hasMore()).toBe(false); + }); + + it('reads little-endian unsigned ints', () => { + const r16 = new BufferReader(new Uint8Array([0x34, 0x12])); + expect(r16.readUint16LE()).toBe(0x1234); + + const r32 = new BufferReader(new Uint8Array([0x78, 0x56, 0x34, 0x12])); + expect(r32.readUint32LE()).toBe(0x12345678); + }); + + it('reads signed ints with two/four bytes (negative)', () => { + const r16 = new BufferReader(new Uint8Array([0xff, 0xff])); + expect(r16.readInt16LE()).toBe(-1); + + const r32 = new BufferReader(new Uint8Array([0xff, 0xff, 0xff, 0xff])); + expect(r32.readInt32LE()).toBe(-1); + }); + + it('readTimestamp returns Date with seconds->ms conversion', () => { + const r = new BufferReader(new Uint8Array([0x01, 0x00, 0x00, 0x00])); + const d = r.readTimestamp(); + expect(d.getTime()).toBe(1000); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8b991a7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "ESNext", + "moduleResolution": "Node", + "declaration": true, + "declarationMap": false, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "test"] +}