commit 5d537975e9a95169853fb72a7e68bf2f9092ee8d Author: maze Date: Wed Mar 11 17:24:57 2026 +0100 Initial release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a61cd08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,276 @@ +# 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 +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.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 new file mode 100644 index 0000000..e69de29 diff --git a/docs/APRS12c.pdf b/docs/APRS12c.pdf new file mode 100644 index 0000000..2f9e657 Binary files /dev/null and b/docs/APRS12c.pdf differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c6df6b2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,19 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1b26413 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3283 @@ +{ + "name": "@hamradio/aprs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@hamradio/aprs", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@vitest/coverage-v8": "^4.0.18", + "eslint": "^10.0.3", + "globals": "^17.4.0", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + "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/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "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/@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/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "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/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@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", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e99dbcc --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "@hamradio/aprs", + "version": "1.0.0", + "description": "APRS (Automatic Packet Reporting System) protocol support for Typescript", + "keywords": [ + "APRS", + "radio", + "amateur radio", + "HAM radio", + "rf" + ], + "repository": { + "type": "git", + "url": "https://git.maze.io/ham/aprs.js" + }, + "license": "MIT", + "author": "Wijnand Modderman-Lenstra", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "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", + "lint": "eslint .", + "prepare": "npm run build" + }, + "dependencies": {}, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@vitest/coverage-v8": "^4.0.18", + "eslint": "^10.0.3", + "globals": "^17.4.0", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + "vitest": "^4.0.18" + } +} diff --git a/src/frame.ts b/src/frame.ts new file mode 100644 index 0000000..8a3893b --- /dev/null +++ b/src/frame.ts @@ -0,0 +1,1109 @@ +import type { IAddress, IFrame, Payload, ITimestamp, PositionPayload, IPosition } from "./frame.types" +import { type PacketStructure, type PacketSegment, type PacketField, FieldType } from "./parser.types" +import { Position } from "./position"; +import { base91ToNumber } from "./parser" + +export class Timestamp implements ITimestamp { + day?: number; + month?: number; + hours: number; + minutes: number; + seconds?: number; + format: 'DHM' | 'HMS' | 'MDHM'; + zulu?: boolean; + + constructor( + hours: number, + minutes: number, + format: 'DHM' | 'HMS' | 'MDHM', + options: { + day?: number; + month?: number; + seconds?: number; + zulu?: boolean; + } = {} + ) { + this.hours = hours; + this.minutes = minutes; + this.format = format; + this.day = options.day; + this.month = options.month; + this.seconds = options.seconds; + this.zulu = options.zulu; + } + + /** + * Convert APRS timestamp to JavaScript Date object + * Note: APRS timestamps don't include year, so we use current year + * For DHM format, we find the most recent occurrence of that day + * For HMS format, we use current date + * For MDHM format, we use the specified month/day in current year + */ + toDate(): Date { + const now = new Date(); + + if (this.format === 'DHM') { + // Day-Hour-Minute format (UTC) + // Find the most recent occurrence of this day + const currentYear = this.zulu ? now.getUTCFullYear() : now.getFullYear(); + const currentMonth = this.zulu ? now.getUTCMonth() : now.getMonth(); + + let date: Date; + if (this.zulu) { + date = new Date(Date.UTC(currentYear, currentMonth, this.day!, this.hours, this.minutes, 0, 0)); + } else { + date = new Date(currentYear, currentMonth, this.day!, this.hours, this.minutes, 0, 0); + } + + // If the date is in the future, it's from last month + if (date > now) { + if (this.zulu) { + date = new Date(Date.UTC(currentYear, currentMonth - 1, this.day!, this.hours, this.minutes, 0, 0)); + } else { + date = new Date(currentYear, currentMonth - 1, this.day!, this.hours, this.minutes, 0, 0); + } + } + + return date; + } else if (this.format === 'HMS') { + // Hour-Minute-Second format (UTC) + // Use current date + if (this.zulu) { + const date = new Date(); + date.setUTCHours(this.hours, this.minutes, this.seconds || 0, 0); + + // If time is in the future, it's from yesterday + if (date > now) { + date.setUTCDate(date.getUTCDate() - 1); + } + + return date; + } else { + const date = new Date(); + date.setHours(this.hours, this.minutes, this.seconds || 0, 0); + + if (date > now) { + date.setDate(date.getDate() - 1); + } + + return date; + } + } else { + // MDHM format: Month-Day-Hour-Minute (local time) + const currentYear = now.getFullYear(); + let date = new Date(currentYear, (this.month || 1) - 1, this.day!, this.hours, this.minutes, 0, 0); + + // If date is in the future, it's from last year + if (date > now) { + date = new Date(currentYear - 1, (this.month || 1) - 1, this.day!, this.hours, this.minutes, 0, 0); + } + + return date; + } + } +} + +export class Address implements IAddress { + call: string; + ssid: string = ""; + isRepeated: boolean = false; + + constructor(call: string, ssid: string | number = "", isRepeated: boolean = false) { + this.call = call; + if (typeof ssid === 'number') { + this.ssid = ssid.toString(); + } else if (typeof ssid === 'string') { + this.ssid = ssid; + } else { + throw new Error("SSID must be a string or number"); + } + if (typeof isRepeated !== 'boolean') { + throw new Error("isRepeated must be a boolean"); + } + this.isRepeated = isRepeated || false; + } + + public toString(): string { + return `${this.call}${this.ssid ? '-' + this.ssid : ''}${this.isRepeated ? '*' : ''}`; + } + + public static fromString(addr: string): Address { + const isRepeated = addr.endsWith('*'); + + const baseAddr = isRepeated ? addr.slice(0, -1) : addr; + const parts = baseAddr.split('-'); + const call = parts[0]; + const ssid = parts.length > 1 ? parts[1] : ''; + + return new Address(call, ssid, isRepeated); + } + + public static parse(addr: string): Address { + return Address.fromString(addr); + } +} + +export class Frame implements IFrame { + source: Address; + destination: Address; + path: Address[]; + payload: string; + private _routingSection?: PacketSegment; + + constructor(source: Address, destination: Address, path: Address[], payload: string, routingSection?: PacketSegment) { + this.source = source; + this.destination = destination; + this.path = path; + this.payload = payload; + this._routingSection = routingSection; + } + + /** + * Get the data type identifier (first character of payload) + */ + getDataTypeIdentifier(): string { + return this.payload.charAt(0); + } + + /** + * Get or build routing section from cached data + */ + private getRoutingSection(): PacketSegment | undefined { + return this._routingSection; + } + + /** + * Decode the APRS payload based on its data type identifier + * Returns the decoded payload with optional structure for packet dissection + */ + decode(withStructure?: boolean): Payload | null | { payload: Payload | null; structure: PacketStructure } { + if (!this.payload) { + if (withStructure) { + const structure: PacketStructure = []; + const routingSection = this.getRoutingSection(); + if (routingSection) { + structure.push(routingSection); + + // Add data type identifier section + structure.push({ + name: 'Data Type Identifier', + data: new TextEncoder().encode(this.payload.charAt(0)), + fields: [ + { type: FieldType.CHAR, name: 'Identifier', size: 1 }, + ], + }); + } + return { payload: null, structure }; + } + return null; + } + + const dataType = this.getDataTypeIdentifier(); + let decodedPayload: Payload | null = null; + let payloadsegment: PacketSegment[] | undefined = undefined; + + // TODO: Implement full decoding logic for each payload type + switch (dataType) { + case '!': // Position without timestamp, no messaging + case '=': // Position without timestamp, with messaging + case '/': // Position with timestamp, no messaging + case '@': // Position with timestamp, with messaging + ({ payload: decodedPayload, segment: payloadsegment } = this.decodePosition(dataType, withStructure)); + break; + + case '`': // Mic-E current + case "'": // Mic-E old + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeMicE(withStructure)); + break; + + case ':': // Message + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeMessage(withStructure)); + break; + + case ';': // Object + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeObject(withStructure)); + break; + + case ')': // Item + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeItem(withStructure)); + break; + + case '>': // Status + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeStatus(withStructure)); + break; + + case '?': // Query + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeQuery(withStructure)); + break; + + case 'T': // Telemetry + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeTelemetry(withStructure)); + break; + + case '_': // Weather without position + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeWeather(withStructure)); + break; + + case '$': // Raw GPS + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeRawGPS(withStructure)); + break; + + case '<': // Station capabilities + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeCapabilities(withStructure)); + break; + + case '{': // User-defined + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeUserDefined(withStructure)); + break; + + case '}': // Third-party + ({ payload: decodedPayload, segment: payloadsegment } = this.decodeThirdParty(withStructure)); + break; + + default: + decodedPayload = null; + } + + if (withStructure) { + const structure: PacketStructure = []; + const routingSection = this.getRoutingSection(); + if (routingSection) { + structure.push(routingSection); + } + if (payloadsegment) { + structure.push(...payloadsegment); + } + return { payload: decodedPayload, structure }; + } + + return decodedPayload; + } + + private decodePosition(dataType: string, withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + try { + const hasTimestamp = dataType === '/' || dataType === '@'; + const messaging = dataType === '=' || dataType === '@'; + let offset = 1; // Skip data type identifier + + // Build structure as we parse + const structure: PacketSegment[] = withStructure ? [] : []; + + let timestamp: Timestamp | undefined = undefined; + + // Parse timestamp if present (7 characters: DDHHMMz or HHMMSSh or MMDDHMMM) + if (hasTimestamp) { + if (this.payload.length < 8) return { payload: null }; + const timestampOffset = offset; + const timeStr = this.payload.substring(offset, offset + 7); + const { timestamp: parsedTimestamp, segment: timestampSegment } = this.parseTimestamp(timeStr, withStructure, timestampOffset); + timestamp = parsedTimestamp; + + if (timestampSegment) { + structure.push(timestampSegment); + } + + offset += 7; + } + + if (this.payload.length < offset + 19) return { payload: null }; + + // Check if compressed format + const positionOffset = offset; + const isCompressed = this.isCompressedPosition(this.payload.substring(offset)); + + let position: Position; + let comment = ''; + + if (isCompressed) { + // Compressed format: /YYYYXXXX$csT + const { position: compressed, segment: compressedSegment } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + if (!compressed) return { payload: null }; + + position = new Position({ + latitude: compressed.latitude, + longitude: compressed.longitude, + symbol: compressed.symbol, + }); + + if (compressed.altitude !== undefined) { + position.altitude = compressed.altitude; + } + + if (compressedSegment) { + structure.push(compressedSegment); + } + + offset += 13; // Compressed position is 13 chars + comment = this.payload.substring(offset); + } else { + // Uncompressed format: DDMMmmH/DDDMMmmH$ + const { position: uncompressed, segment: uncompressedSegment } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + if (!uncompressed) return { payload: null }; + + position = new Position({ + latitude: uncompressed.latitude, + longitude: uncompressed.longitude, + symbol: uncompressed.symbol, + }); + + if (uncompressed.ambiguity !== undefined) { + position.ambiguity = uncompressed.ambiguity; + } + + if (uncompressedSegment) { + structure.push(uncompressedSegment); + } + + offset += 19; // Uncompressed position is 19 chars + comment = this.payload.substring(offset); + } + + // Parse altitude from comment if present (format: /A=NNNNNN) + const altMatch = comment.match(/\/A=(\d{6})/); + if (altMatch) { + position.altitude = parseInt(altMatch[1], 10) * 0.3048; // feet to meters + } + + if (comment) { + position.comment = comment; + + // Emit comment section as we parse + if (withStructure) { + structure.push({ + name: 'comment', + data: new TextEncoder().encode(comment), + fields: [ + { type: FieldType.STRING, name: 'text', size: comment.length }, + ], + }); + } + } + + const payload: PositionPayload = { + type: 'position', + timestamp, + position, + messaging, + }; + + if (withStructure) { + return { payload, segment: structure }; + } + + return { payload }; + } catch (e) { + return { payload: null }; + } + } + + private parseTimestamp(timeStr: string, withStructure: boolean = false, offset: number = 0): { timestamp: Timestamp | undefined; segment?: PacketSegment } { + if (timeStr.length !== 7) return { timestamp: undefined }; + + const timeType = timeStr.charAt(6); + + if (timeType === 'z') { + // DHM format: Day-Hour-Minute (UTC) + const timestamp = new Timestamp( + parseInt(timeStr.substring(2, 4), 10), + parseInt(timeStr.substring(4, 6), 10), + 'DHM', + { + day: parseInt(timeStr.substring(0, 2), 10), + zulu: true, + } + ); + + const segment = withStructure ? { + name: 'timestamp', + data: new TextEncoder().encode(timeStr), + fields: [ + { type: FieldType.STRING, name: 'day (DD)', size: 2 }, + { type: FieldType.STRING, name: 'hour (HH)', size: 2 }, + { type: FieldType.STRING, name: 'minute (MM)', size: 2 }, + { type: FieldType.CHAR, name: 'timezone indicator', size: 1 }, + ], + } : undefined; + + return { timestamp, segment }; + } else if (timeType === 'h') { + // HMS format: Hour-Minute-Second (UTC) + const timestamp = new Timestamp( + parseInt(timeStr.substring(0, 2), 10), + parseInt(timeStr.substring(2, 4), 10), + 'HMS', + { + seconds: parseInt(timeStr.substring(4, 6), 10), + zulu: true, + } + ); + + const segment = withStructure ? { + name: 'timestamp', + data: new TextEncoder().encode(timeStr), + fields: [ + { type: FieldType.STRING, name: 'hour (HH)', size: 2 }, + { type: FieldType.STRING, name: 'minute (MM)', size: 2 }, + { type: FieldType.STRING, name: 'second (SS)', size: 2 }, + { type: FieldType.CHAR, name: 'timezone indicator', size: 1 }, + ], + } : undefined; + + return { timestamp, segment }; + } else if (timeType === '/') { + // MDHM format: Month-Day-Hour-Minute (local) + const timestamp = new Timestamp( + parseInt(timeStr.substring(4, 6), 10), + parseInt(timeStr.substring(6, 8), 10), + 'MDHM', + { + month: parseInt(timeStr.substring(0, 2), 10), + day: parseInt(timeStr.substring(2, 4), 10), + zulu: false, + } + ); + + const segment = withStructure ? { + name: 'timestamp', + data: new TextEncoder().encode(timeStr), + fields: [ + { type: FieldType.STRING, name: 'month (MM)', size: 2 }, + { type: FieldType.STRING, name: 'day (DD)', size: 2 }, + { type: FieldType.STRING, name: 'hour (HH)', size: 2 }, + { type: FieldType.STRING, name: 'minute (MM)', size: 2 }, + { type: FieldType.CHAR, name: 'timezone indicator', size: 1 }, + ], + } : undefined; + + return { timestamp, segment }; + } + + return { timestamp: undefined }; + } + + private isCompressedPosition(data: string): boolean { + if (data.length < 13) return false; + + // First prefer uncompressed detection by attempting an uncompressed parse. + // Uncompressed APRS positions do not have a fixed symbol table separator; + // position 8 is a symbol table identifier and may vary. + if (data.length >= 19) { + const uncompressed = this.parseUncompressedPosition(data, false, 0); + if (uncompressed.position) { + return false; + } + } + + // For compressed format, check if the position part looks like base-91 encoded data + // Compressed format: STYYYYXXXXcsT where ST is symbol table/code + // Base-91 chars are in range 33-124 (! to |) + const lat1 = data.charCodeAt(1); + const lat2 = data.charCodeAt(2); + const lon1 = data.charCodeAt(5); + const lon2 = data.charCodeAt(6); + + return lat1 >= 33 && lat1 <= 124 && + lat2 >= 33 && lat2 <= 124 && + lon1 >= 33 && lon1 <= 124 && + lon2 >= 33 && lon2 <= 124; + } + + private parseCompressedPosition(data: string, withStructure: boolean = false, offset: number = 0): { position: { latitude: number; longitude: number; symbol: any; altitude?: number } | null; segment?: PacketSegment } { + if (data.length < 13) return { position: null }; + + const symbolTable = data.charAt(0); + const symbolCode = data.charAt(9); + + // Extract base-91 encoded position (4 characters each) + const latStr = data.substring(1, 5); + const lonStr = data.substring(5, 9); + + try { + // Decode base-91 encoded latitude and longitude + const latBase91 = base91ToNumber(latStr); + const lonBase91 = base91ToNumber(lonStr); + + // Convert to degrees + const latitude = 90 - (latBase91 / 380926); + const longitude = -180 + (lonBase91 / 190463); + + const result: any = { + latitude, + longitude, + symbol: { + table: symbolTable, + code: symbolCode, + }, + }; + + // Check for compressed altitude (csT format) + const cs = data.charAt(10); + const t = data.charCodeAt(11); + + if (cs === ' ' && t >= 33 && t <= 124) { + // Compressed altitude: altitude = 1.002^(t-33) feet + const altFeet = Math.pow(1.002, t - 33); + result.altitude = altFeet * 0.3048; // Convert to meters + } + + const section: PacketSegment | undefined = withStructure ? { + name: 'position', + data: new TextEncoder().encode(data.substring(0, 13)), + fields: [ + { type: FieldType.CHAR, size: 1, name: 'symbol table' }, + { type: FieldType.STRING, size: 4, name: 'latitude' }, + { type: FieldType.STRING, size: 4, name: 'longitude' }, + { type: FieldType.CHAR, size: 1, name: 'symbol code' }, + { type: FieldType.CHAR, size: 1, name: 'course/speed type' }, + { type: FieldType.CHAR, size: 1, name: 'course/speed value' }, + { type: FieldType.CHAR, size: 1, name: 'altitude' }, + ], + } : undefined; + + return { position: result, segment: section }; + } catch (e) { + return { position: null }; + } + } + + private parseUncompressedPosition(data: string, withStructure: boolean = false, offset: number = 0): { position: { latitude: number; longitude: number; symbol: any; ambiguity?: number } | null; segment?: PacketSegment } { + if (data.length < 19) return { position: null }; + + // Format: DDMMmmH/DDDMMmmH$ where H is hemisphere, $ is symbol code + // Positions: 0-7 (latitude), 8 (symbol table), 9-17 (longitude), 18 (symbol code) + // Spaces may replace rightmost digits for ambiguity/privacy + + const latStr = data.substring(0, 8); // DDMMmmH (8 chars: 49 03.50 N) + const symbolTable = data.charAt(8); + const lonStr = data.substring(9, 18); // DDDMMmmH (9 chars: 072 01.75 W) + const symbolCode = data.charAt(18); + + // Count and handle ambiguity (spaces in minutes part replace rightmost digits) + let ambiguity = 0; + const latSpaceCount = (latStr.match(/ /g) || []).length; + const lonSpaceCount = (lonStr.match(/ /g) || []).length; + + if (latSpaceCount > 0 || lonSpaceCount > 0) { + // Use the maximum space count (they should be the same, but be defensive) + ambiguity = Math.max(latSpaceCount, lonSpaceCount); + } + + // Replace spaces with zeros for parsing + const latStrNormalized = latStr.replace(/ /g, '0'); + const lonStrNormalized = lonStr.replace(/ /g, '0'); + + // Parse latitude + const latDeg = parseInt(latStrNormalized.substring(0, 2), 10); + const latMin = parseFloat(latStrNormalized.substring(2, 7)); + const latHem = latStrNormalized.charAt(7); + + if (isNaN(latDeg) || isNaN(latMin)) return { position: null }; + if (latHem !== 'N' && latHem !== 'S') return { position: null }; + + let latitude = latDeg + (latMin / 60); + if (latHem === 'S') latitude = -latitude; + + // Parse longitude + const lonDeg = parseInt(lonStrNormalized.substring(0, 3), 10); + const lonMin = parseFloat(lonStrNormalized.substring(3, 8)); + const lonHem = lonStrNormalized.charAt(8); + + if (isNaN(lonDeg) || isNaN(lonMin)) return { position: null }; + if (lonHem !== 'E' && lonHem !== 'W') return { position: null }; + + let longitude = lonDeg + (lonMin / 60); + if (lonHem === 'W') longitude = -longitude; + + const result: any = { + latitude, + longitude, + symbol: { + table: symbolTable, + code: symbolCode, + }, + }; + + if (ambiguity > 0) { + result.ambiguity = ambiguity; + } + + const segment: PacketSegment | undefined = withStructure ? { + name: 'position', + data: new TextEncoder().encode(data.substring(0, 19)), + fields: [ + { type: FieldType.STRING, size: 8, name: 'latitude' }, + { type: FieldType.CHAR, size: 1, name: 'symbol table' }, + { type: FieldType.STRING, size: 9, name: 'longitude' }, + { type: FieldType.CHAR, size: 1, name: 'symbol code' }, + ], + } : undefined; + + return { position: result, segment }; + } + + private decodeMicE(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + try { + // TODO: Add section emission support when withStructure is true + // For now, Mic-E returns payload without structure + + // Mic-E encodes position in both destination address and information field + const dest = this.destination.call; + + if (dest.length < 6) return { payload: null }; + if (this.payload.length < 9) return { payload: null }; // Need at least data type + 8 bytes + + // Decode latitude from destination address (6 characters) + const latResult = this.decodeMicELatitude(dest); + if (!latResult) return { payload: null }; + + const { latitude, messageType, longitudeOffset, isWest, isStandard } = latResult; + + // Parse information field (skip data type identifier at position 0) + let offset = 1; + + // Longitude: 3 bytes (degrees, minutes, hundredths) + const lonDegRaw = this.payload.charCodeAt(offset) - 28; + const lonMinRaw = this.payload.charCodeAt(offset + 1) - 28; + const lonHunRaw = this.payload.charCodeAt(offset + 2) - 28; + offset += 3; + + // Apply longitude offset and hemisphere + let lonDeg = lonDegRaw; + if (longitudeOffset) { + lonDeg += 100; + } + if (lonDeg >= 180 && lonDeg <= 189) { + lonDeg -= 80; + } else if (lonDeg >= 190 && lonDeg <= 199) { + lonDeg -= 190; + } + + let longitude = lonDeg + (lonMinRaw / 60.0) + (lonHunRaw / 6000.0); + if (isWest) { + longitude = -longitude; + } + + // Speed and course: 3 bytes + const sp = this.payload.charCodeAt(offset) - 28; + const dc = this.payload.charCodeAt(offset + 1) - 28; + const se = this.payload.charCodeAt(offset + 2) - 28; + offset += 3; + + let speed = (sp * 10) + Math.floor(dc / 10); // Speed in knots + let course = ((dc % 10) * 100) + se; // Course in degrees + + if (course >= 400) course -= 400; + if (speed >= 800) speed -= 800; + + // Convert speed from knots to km/h + const speedKmh = speed * 1.852; + + // Symbol code and table + if (this.payload.length < offset + 2) return { payload: null }; + const symbolCode = this.payload.charAt(offset); + const symbolTable = this.payload.charAt(offset + 1); + offset += 2; + + // Parse remaining data (altitude, comment, telemetry) + const remaining = this.payload.substring(offset); + let altitude: number | undefined = undefined; + let comment = remaining; + + // Check for altitude in various formats + // Format 1: }xyz where xyz is altitude in base-91 (obsolete) + // Format 2: /A=NNNNNN where NNNNNN is altitude in feet + const altMatch = remaining.match(/\/A=(\d{6})/); + if (altMatch) { + altitude = parseInt(altMatch[1], 10) * 0.3048; // feet to meters + } else if (remaining.startsWith('}')) { + // Base-91 altitude (3 characters after }) + if (remaining.length >= 4) { + try { + const altBase91 = remaining.substring(1, 4); + const altFeet = base91ToNumber(altBase91) - 10000; + altitude = altFeet * 0.3048; // feet to meters + } catch (e) { + // Ignore altitude parsing errors + } + } + } + + const result: any = { + type: 'position', + position: { + latitude, + longitude, + symbol: { + table: symbolTable, + code: symbolCode, + }, + }, + messaging: true, // Mic-E is always messaging-capable + micE: { + messageType, + isStandard, + }, + }; + + if (speed > 0) { + result.position.speed = speedKmh; + } + + if (course > 0 && course < 360) { + result.position.course = course; + } + + if (altitude !== undefined) { + result.position.altitude = altitude; + } + + if (comment) { + result.position.comment = comment; + } + + return { payload: result }; + } catch (e) { + return { payload: null }; + } + } + + private decodeMicELatitude(dest: string): { + latitude: number; + messageType: string; + longitudeOffset: boolean; + isWest: boolean; + isStandard: boolean; + } | null { + if (dest.length < 6) return null; + + // Each destination character encodes a latitude digit and message bits + const digits: number[] = []; + const messageBits: number[] = []; + + for (let i = 0; i < 6; i++) { + const code = dest.charCodeAt(i); + let digit: number; + let msgBit: number; + + if (code >= 48 && code <= 57) { + // '0'-'9' + digit = code - 48; + msgBit = 0; + } else if (code >= 65 && code <= 74) { + // 'A'-'J' (A=0, B=1, ... J=9) + digit = code - 65; + msgBit = 1; + } else if (code === 75) { + // 'K' means space (used for ambiguity) + digit = 0; + msgBit = 1; + } else if (code === 76) { + // 'L' means space + digit = 0; + msgBit = 0; + } else if (code >= 80 && code <= 89) { + // 'P'-'Y' custom message types (P=0, Q=1, R=2, ... Y=9) + digit = code - 80; + msgBit = 1; + } else if (code === 90) { + // 'Z' means space + digit = 0; + msgBit = 1; + } else { + return null; // Invalid character + } + + digits.push(digit); + messageBits.push(msgBit); + } + + // Decode latitude: format is DDMM.HH (degrees, minutes, hundredths) + const latDeg = digits[0] * 10 + digits[1]; + const latMin = digits[2] * 10 + digits[3]; + const latHun = digits[4] * 10 + digits[5]; + + let latitude = latDeg + (latMin / 60.0) + (latHun / 6000.0); + + // Message bits determine hemisphere and other flags + // Bit 3 (messageBits[3]): 0 = North, 1 = South + // Bit 4 (messageBits[4]): 0 = West, 1 = East + // Bit 5 (messageBits[5]): 0 = longitude offset +0, 1 = longitude offset +100 + const isNorth = messageBits[3] === 0; + const isWest = messageBits[4] === 0; + const longitudeOffset = messageBits[5] === 1; + + if (!isNorth) { + latitude = -latitude; + } + + // Decode message type from bits 0, 1, 2 + const msgValue = messageBits[0] * 4 + messageBits[1] * 2 + messageBits[2]; + const messageTypes = [ + 'M0: Off Duty', + 'M1: En Route', + 'M2: In Service', + 'M3: Returning', + 'M4: Committed', + 'M5: Special', + 'M6: Priority', + 'M7: Emergency', + ]; + const messageType = messageTypes[msgValue] || 'Unknown'; + + // Standard vs custom message indicator + const isStandard = messageBits[0] === 1; + + return { + latitude, + messageType, + longitudeOffset, + isWest, + isStandard, + }; + } + + private decodeMessage(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement message decoding with section emission + // When implemented, build structure during parsing like decodePosition does + return { payload: null }; + } + + private decodeObject(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + try { + // Object format: ;AAAAAAAAAcDDHHMMzDDMM.hhN/DDDMM.hhW$comment + // ^ data type + // 9-char name + // alive (*) / killed (_) + if (this.payload.length < 18) return { payload: null }; // 1 + 9 + 1 + 7 minimum + + let offset = 1; // Skip data type identifier ';' + const segment: PacketSegment[] = withStructure ? [] : []; + + const rawName = this.payload.substring(offset, offset + 9); + const name = rawName.trimEnd(); + if (withStructure) { + segment.push({ + name: 'object name', + data: new TextEncoder().encode(rawName), + fields: [ + { type: FieldType.STRING, name: 'name', size: 9 }, + ], + }); + } + offset += 9; + + const stateChar = this.payload.charAt(offset); + if (stateChar !== '*' && stateChar !== '_') { + return { payload: null }; + } + const alive = stateChar === '*'; + if (withStructure) { + segment.push({ + name: 'object state', + data: new TextEncoder().encode(stateChar), + fields: [ + { type: FieldType.CHAR, name: 'State (* alive, _ killed)', size: 1 }, + ], + }); + } + offset += 1; + + const timeStr = this.payload.substring(offset, offset + 7); + const { timestamp, segment: timestampSection } = this.parseTimestamp(timeStr, withStructure, offset); + if (!timestamp) { + return { payload: null }; + } + if (timestampSection) { + segment.push(timestampSection); + } + offset += 7; + + const positionOffset = offset; + const isCompressed = this.isCompressedPosition(this.payload.substring(offset)); + + let position: { latitude: number; longitude: number; symbol: any; ambiguity?: number; altitude?: number; comment?: string } | null = null; + let consumed = 0; + + if (isCompressed) { + const { position: compressed, segment: compressedSection } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + if (!compressed) return { payload: null }; + + position = { + latitude: compressed.latitude, + longitude: compressed.longitude, + symbol: compressed.symbol, + altitude: compressed.altitude, + }; + consumed = 13; + + if (compressedSection) { + segment.push(compressedSection); + } + } else { + const { position: uncompressed, segment: uncompressedSection } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + if (!uncompressed) return { payload: null }; + + position = { + latitude: uncompressed.latitude, + longitude: uncompressed.longitude, + symbol: uncompressed.symbol, + ambiguity: uncompressed.ambiguity, + }; + consumed = 19; + + if (uncompressedSection) { + segment.push(uncompressedSection); + } + } + + offset += consumed; + const comment = this.payload.substring(offset); + if (comment) { + position.comment = comment; + + if (withStructure) { + segment.push({ + name: 'Comment', + data: new TextEncoder().encode(comment), + fields: [ + { type: FieldType.STRING, name: 'text', size: comment.length }, + ], + }); + } + } + + const payload: any = { + type: 'object', + name, + timestamp, + alive, + position, + }; + + if (withStructure) { + return { payload, segment }; + } + + return { payload }; + } catch (e) { + return { payload: null }; + } + } + + private decodeItem(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement item decoding with section emission + return { payload: null }; + } + + private decodeStatus(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement status decoding with section emission + return { payload: null }; + } + + private decodeQuery(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement query decoding with section emission + return { payload: null }; + } + + private decodeTelemetry(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement telemetry decoding with section emission + return { payload: null }; + } + + private decodeWeather(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement weather decoding with section emission + return { payload: null }; + } + + private decodeRawGPS(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement raw GPS decoding with section emission + return { payload: null }; + } + + private decodeCapabilities(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement capabilities decoding with section emission + return { payload: null }; + } + + private decodeUserDefined(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement user-defined decoding with section emission + return { payload: null }; + } + + private decodeThirdParty(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + // TODO: Implement third-party decoding with section emission + return { payload: null }; + } + + public static fromString(data: string): Frame { + return parseFrame(data); + } + + public static parse(data: string): Frame { + return parseFrame(data); + } +} + +const parseFrame = (data: string): Frame => { + const encoder = new TextEncoder(); + + const routeSepIndex = data.indexOf(':'); + if (routeSepIndex === -1) { + throw new Error('APRS: invalid frame, no route separator found'); + } + + const route = data.slice(0, routeSepIndex); + const payload = data.slice(routeSepIndex + 1); + const parts = route.split('>'); + if (parts.length < 2) { + throw new Error('APRS: invalid addresses in route'); + } + + // Parse source - track byte offset as we parse + let offset = 0; + const sourceStr = parts[0]; + const source = Address.fromString(sourceStr); + + offset += sourceStr.length + 1; // +1 for '>' + + // Parse destination and path + const destinationAndPath = parts[1].split(','); + const destinationStr = destinationAndPath[0]; + const destination = Address.fromString(destinationStr); + + offset += destinationStr.length; + + // Parse path + const path: Address[] = []; + const pathFields: PacketField[] = []; + for (let i = 1; i < destinationAndPath.length; i++) { + offset += 1; // +1 for ',' + const pathStr = destinationAndPath[i]; + path.push(Address.fromString(pathStr)); + + pathFields.push({ + type: FieldType.CHAR, + name: `Path separator ${i}`, + size: 1 + }); + pathFields.push({ + type: FieldType.STRING, + name: `Repeater ${i}`, + size: pathStr.length, + }); + offset += pathStr.length; + } + + const routingSection: PacketSegment = { + name: 'Routing', + data: encoder.encode(data.slice(0, routeSepIndex)), + fields: [ + { type: FieldType.STRING, name: 'Source address', size: sourceStr.length }, + { type: FieldType.CHAR, name: 'Route separator', size: 1 }, + { type: FieldType.STRING, name: 'Destination address', size: destinationStr.length }, + ...pathFields, + { type: FieldType.CHAR, name: 'Payload separator', size: 1 }, + ], + }; + + return new Frame(source, destination, path, payload, routingSection); +} diff --git a/src/frame.types.ts b/src/frame.types.ts new file mode 100644 index 0000000..399b688 --- /dev/null +++ b/src/frame.types.ts @@ -0,0 +1,325 @@ +import { PacketSegment, PacketStructure } from "./parser.types"; + +export interface IAddress { + call: string; + ssid: string; + isRepeated: boolean; +} + +export interface IFrame { + source: IAddress; + destination: IAddress; + path: IAddress[]; + payload: string; +} + +// APRS Data Type Identifiers (first character of payload) +export const DataTypeIdentifier = { + // Position Reports + PositionNoTimestampNoMessaging: '!', + PositionNoTimestampWithMessaging: '=', + PositionWithTimestampNoMessaging: '/', + PositionWithTimestampWithMessaging: '@', + + // Mic-E + MicECurrent: '`', + MicEOld: "'", + + // Messages and Bulletins + Message: ':', + + // Objects and Items + Object: ';', + Item: ')', + + // Status + Status: '>', + + // Query + Query: '?', + + // Telemetry + TelemetryData: 'T', + + // Weather + WeatherReportNoPosition: '_', + + // Raw GPS Data + RawGPS: '$', + + // Station Capabilities + StationCapabilities: '<', + + // User-Defined + UserDefined: '{', + + // Third-Party Traffic + ThirdParty: '}', + + // Invalid/Test Data + InvalidOrTest: ',', +} as const; + +export type DataTypeIdentifier = typeof DataTypeIdentifier[keyof typeof DataTypeIdentifier]; + +export interface ISymbol { + table: string; // Symbol table identifier + code: string; // Symbol code + + toString(): string; // Return combined symbol representation (e.g., "tablecode") +} + +// Position data common to multiple formats +export interface IPosition { + latitude: number; // Decimal degrees + longitude: number; // Decimal degrees + ambiguity?: number; // Position ambiguity (0-4) + altitude?: number; // Meters + speed?: number; // Speed in knots/kmh depending on source + course?: number; // Course in degrees + symbol?: ISymbol; + comment?: string; + + toString(): string; // Return combined position representation (e.g., "lat,lon,alt") + toCompressed?(): CompressedPosition; // Optional method to convert to compressed format + distanceTo?(other: IPosition): number; // Optional method to calculate distance to another position +} + +export interface ITimestamp { + day?: number; // Day of month (DHM format) + month?: number; // Month (MDHM format) + hours: number; + minutes: number; + seconds?: number; + format: 'DHM' | 'HMS' | 'MDHM'; // Day-Hour-Minute, Hour-Minute-Second, Month-Day-Hour-Minute + zulu?: boolean; // Is UTC/Zulu time + toDate(): Date; // Convert to Date object respecting timezone +} + +// Position Report Payload +export interface PositionPayload { + type: 'position'; + timestamp?: ITimestamp; + position: IPosition; + messaging: boolean; // Whether APRS messaging is enabled + micE?: { + messageType?: string; + isStandard?: boolean; + }; + sections?: PacketSegment[]; +} + +// Compressed Position Format +export interface CompressedPosition { + latitude: number; + longitude: number; + symbol: { + table: string; + code: string; + }; + course?: number; // Degrees + speed?: number; // Knots + range?: number; // Miles + altitude?: number; // Feet + radioRange?: number; // Miles + compression: 'old' | 'current'; +} + +// Mic-E Payload (compressed in destination address) +export interface MicEPayload { + type: 'mic-e'; + position: IPosition; + course?: number; + speed?: number; + altitude?: number; + messageType?: string; // Standard Mic-E message + telemetry?: number[]; // Optional telemetry channels + status?: string; +} + +// Message Payload +export interface MessagePayload { + type: 'message'; + addressee: string; // 9 character padded callsign + text: string; // Message text + messageNumber?: string; // Message ID for acknowledgment + ack?: string; // Acknowledgment of message ID + reject?: string; // Rejection of message ID +} + +// Bulletin/Announcement (variant of message) +export interface BulletinPayload { + type: 'bulletin'; + bulletinId: string; // Bulletin identifier (BLN#) + text: string; + group?: string; // Optional group bulletin +} + +// Object Payload +export interface ObjectPayload { + type: 'object'; + name: string; // 9 character object name + timestamp: ITimestamp; + alive: boolean; // True if object is active, false if killed + position: IPosition; + course?: number; + speed?: number; +} + +// Item Payload +export interface ItemPayload { + type: 'item'; + name: string; // 3-9 character item name + alive: boolean; // True if item is active, false if killed + position: IPosition; +} + +// Status Payload +export interface StatusPayload { + type: 'status'; + timestamp?: ITimestamp; + text: string; + maidenhead?: string; // Optional Maidenhead grid locator + symbol?: { + table: string; + code: string; + }; +} + +// Query Payload +export interface QueryPayload { + type: 'query'; + queryType: string; // e.g., 'APRSD', 'APRST', 'PING' + target?: string; // Target callsign or area +} + +// Telemetry Data Payload +export interface TelemetryDataPayload { + type: 'telemetry-data'; + sequence: number; + analog: number[]; // Up to 5 analog channels + digital: number; // 8-bit digital value +} + +// Telemetry Parameter Names +export interface TelemetryParameterPayload { + type: 'telemetry-parameters'; + names: string[]; // Parameter names +} + +// Telemetry Unit/Label +export interface TelemetryUnitPayload { + type: 'telemetry-units'; + units: string[]; // Units for each parameter +} + +// Telemetry Coefficients +export interface TelemetryCoefficientsPayload { + type: 'telemetry-coefficients'; + coefficients: { + a: number[]; // a coefficients + b: number[]; // b coefficients + c: number[]; // c coefficients + }; +} + +// Telemetry Bit Sense/Project Name +export interface TelemetryBitSensePayload { + type: 'telemetry-bitsense'; + sense: number; // 8-bit sense value + projectName?: string; +} + +// Weather Report Payload +export interface WeatherPayload { + type: 'weather'; + timestamp?: ITimestamp; + position?: IPosition; + windDirection?: number; // Degrees + windSpeed?: number; // MPH + windGust?: number; // MPH + temperature?: number; // Fahrenheit + rainLastHour?: number; // Hundredths of inch + rainLast24Hours?: number; // Hundredths of inch + rainSinceMidnight?: number; // Hundredths of inch + humidity?: number; // Percent + pressure?: number; // Tenths of millibar + luminosity?: number; // Watts per square meter + snowfall?: number; // Inches + rawRain?: number; // Raw rain counter + software?: string; // Weather software type + weatherUnit?: string; // Weather station type +} + +// Raw GPS Payload (NMEA sentences) +export interface RawGPSPayload { + type: 'raw-gps'; + sentence: string; // Raw NMEA sentence +} + +// Station Capabilities Payload +export interface StationCapabilitiesPayload { + type: 'capabilities'; + capabilities: string[]; +} + +// User-Defined Payload +export interface UserDefinedPayload { + type: 'user-defined'; + userPacketType: string; + data: string; +} + +// Third-Party Traffic Payload +export interface ThirdPartyPayload { + type: 'third-party'; + header: string; // Source path of third-party packet + payload: string; // Nested APRS packet +} + +// DF Report Payload +export interface DFReportPayload { + type: 'df-report'; + timestamp?: ITimestamp; + position: IPosition; + course?: number; + bearing?: number; // Direction finding bearing + quality?: number; // Signal quality + strength?: number; // Signal strength + height?: number; // Antenna height + gain?: number; // Antenna gain + directivity?: string; // Antenna directivity pattern +} + +export interface BasePayload { + type: string; +} + +// Union type for all decoded payload types +export type Payload = BasePayload & ( + | PositionPayload + | MicEPayload + | MessagePayload + | BulletinPayload + | ObjectPayload + | ItemPayload + | StatusPayload + | QueryPayload + | TelemetryDataPayload + | TelemetryParameterPayload + | TelemetryUnitPayload + | TelemetryCoefficientsPayload + | TelemetryBitSensePayload + | WeatherPayload + | RawGPSPayload + | StationCapabilitiesPayload + | UserDefinedPayload + | ThirdPartyPayload + | DFReportPayload +); + +// Extended Frame with decoded payload +export interface DecodedFrame extends IFrame { + decoded?: Payload; + structure?: PacketStructure; // Routing and other frame-level sections +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5217f4c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,57 @@ +export { + Frame, + Address, + Timestamp, +} from "./frame"; + +export { + type IAddress, + type IFrame, + DataTypeIdentifier, +} from "./frame.types"; + +export { + type ISymbol, + type IPosition, + type ITimestamp, + type PositionPayload, + type CompressedPosition, + type MicEPayload, + type MessagePayload, + type BulletinPayload, + type ObjectPayload, + type ItemPayload, + type StatusPayload, + type QueryPayload, + type TelemetryDataPayload, + type TelemetryParameterPayload, + type TelemetryUnitPayload, + type TelemetryCoefficientsPayload, + type TelemetryBitSensePayload, + type WeatherPayload, + type RawGPSPayload, + type StationCapabilitiesPayload, + type UserDefinedPayload, + type ThirdPartyPayload, + type DFReportPayload, + type BasePayload, + type Payload, + type DecodedFrame, +} from "./frame.types"; + +export { + base91ToNumber, + knotsToKmh, + kmhToKnots, + feetToMeters, + metersToFeet, + celsiusToFahrenheit, + fahrenheitToCelsius, +} from "./parser"; +export { + type PacketStructure, + type PacketSegment, + type PacketField, + type PacketFieldBit, + FieldType, +} from "./parser.types"; diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..2b0c9f5 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,91 @@ +/** + * Decode a Base91 encoded string to a number. + * Base91 uses ASCII characters 33-123 (! to {) for encoding. + * Each character represents a digit in base 91. + * + * @param str Base91 encoded string (typically 4 characters for 32-bit values in APRS) + * @returns Decoded number value + */ +export const base91ToNumber = (str: string): number => { + let value = 0; + const base = 91; + + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + const digit = charCode - 33; // Base91 uses chars 33-123 (! to {) + + if (digit < 0 || digit >= base) { + throw new Error(`Invalid Base91 character: '${str[i]}' (code ${charCode})`); + } + + value = value * base + digit; + } + + return value; +} + +/* Conversions from Freedom Units to whatever the rest of the world uses and understands. */ + +const KNOTS_TO_KMH = 1.852; +const FEET_TO_METERS = 0.3048; +const FAHRENHEIT_TO_CELSIUS_OFFSET = 32; + +/** + * Convert speed from knots to kilometers per hour. + * + * @param knots number of knots + * @returns equivalent speed in kilometers per hour + */ +export const knotsToKmh = (knots: number): number => { + return knots * KNOTS_TO_KMH; +} + +/** + * Convert speed from kilometers per hour to knots. + * + * @param kmh speed in kilometers per hour + * @returns equivalent speed in knots + */ +export const kmhToKnots = (kmh: number): number => { + return kmh / KNOTS_TO_KMH; +} + +/** + * Convert altitude from feet to meters. + * + * @param feet altitude in feet + * @returns equivalent altitude in meters + */ +export const feetToMeters = (feet: number): number => { + return feet * FEET_TO_METERS; +} + +/** + * Convert altitude from meters to feet. + * + * @param meters altitude in meters + * @returns equivalent altitude in feet + */ +export const metersToFeet = (meters: number): number => { + return meters / FEET_TO_METERS; +} + +/** + * Convert temperature from Celsius to Fahrenheit. + * + * @param celsius temperature in Celsius + * @returns equivalent temperature in Fahrenheit + */ +export const celsiusToFahrenheit = (celsius: number): number => { + return (celsius * 9/5) + FAHRENHEIT_TO_CELSIUS_OFFSET; +} + +/** + * Convert temperature from Fahrenheit to Celsius. + * + * @param fahrenheit temperature in Fahrenheit + * @returns equivalent temperature in Celsius + */ +export const fahrenheitToCelsius = (fahrenheit: number): number => { + return (fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5/9; +} diff --git a/src/parser.types.ts b/src/parser.types.ts new file mode 100644 index 0000000..569419f --- /dev/null +++ b/src/parser.types.ts @@ -0,0 +1,37 @@ +export enum FieldType { + BITS = 0, + UINT8 = 1, + UINT16_LE = 2, + UINT16_BE = 3, + UINT32_LE = 4, + UINT32_BE = 5, + BYTES = 6, // 8-bits per value + WORDS = 7, // 16-bits per value + DWORDS = 8, // 32-bits per value + QWORDS = 9, // 64-bits per value + STRING = 10, + C_STRING = 11, // Null-terminated string + CHAR = 12, // Single ASCII character +} + +// Interface for the parsed packet segments, used for debugging and testing. +export type PacketStructure = PacketSegment[]; + +export interface PacketSegment { + name: string; + data: Uint8Array; + fields: PacketField[]; +} + +export interface PacketField { + type: FieldType; + size: number; // Size in bytes + name?: string; + bits?: PacketFieldBit[]; // Only for bit fields in FieldType.BITS + value?: any; // Optional decoded value +} + +export interface PacketFieldBit { + name: string; + size: number; // Size in bits +} diff --git a/src/position.ts b/src/position.ts new file mode 100644 index 0000000..9b61878 --- /dev/null +++ b/src/position.ts @@ -0,0 +1,72 @@ +import { IPosition, ISymbol } from "./frame.types"; + +export class Symbol implements ISymbol { + table: string; // Symbol table identifier + code: string; // Symbol code + + constructor(table: string, code?: string) { + if (code === undefined) { + if (table.length === 2) { + this.code = table[1]; + this.table = table[0]; + } else { + throw new Error(`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`); + } + } else { + this.table = table; + this.code = code; + } + } + + public toString(): string { + return `${this.table}${this.code}`; + } +} + +export class Position implements IPosition { + latitude: number; // Decimal degrees + longitude: number; // Decimal degrees + ambiguity?: number; // Position ambiguity (0-4) + altitude?: number; // Meters + speed?: number; // Speed in knots/kmh depending on source + course?: number; // Course in degrees + symbol?: Symbol; + comment?: string; + + constructor(data: Partial) { + this.latitude = data.latitude ?? 0; + this.longitude = data.longitude ?? 0; + this.ambiguity = data.ambiguity; + this.altitude = data.altitude; + this.speed = data.speed; + this.course = data.course; + if (typeof data.symbol === 'string') { + this.symbol = new Symbol(data.symbol); + } else if (data.symbol) { + this.symbol = new Symbol(data.symbol.table, data.symbol.code); + } + this.comment = data.comment; + } + + public toString(): string { + const latStr = this.latitude.toFixed(5); + const lonStr = this.longitude.toFixed(5); + const altStr = this.altitude !== undefined ? `,${this.altitude}m` : ''; + return `${latStr},${lonStr}${altStr}`; + } + + public distanceTo(other: IPosition): number { + const R = 6371e3; // Earth radius in meters + const φ1 = this.latitude * Math.PI / 180; + const φ2 = other.latitude * Math.PI / 180; + const Δφ = (other.latitude - this.latitude) * Math.PI / 180; + const Δλ = (other.longitude - this.longitude) * Math.PI / 180; + + const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(Δλ/2) * Math.sin(Δλ/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return R * c; // Distance in meters + } +} diff --git a/test/frame.test.ts b/test/frame.test.ts new file mode 100644 index 0000000..98c1641 --- /dev/null +++ b/test/frame.test.ts @@ -0,0 +1,1125 @@ +import { describe, expect, it } from 'vitest'; +import { Address, Frame, Timestamp } from '../src/frame'; +import { Payload, PositionPayload } from '../src/frame.types'; +import { FieldType, PacketSegment, PacketStructure } from '../src/parser.types'; + +describe('parseAddress', () => { + it('should parse callsign without SSID', () => { + const result = Address.parse('NOCALL'); + expect(result).toEqual({ + call: 'NOCALL', + ssid: '', + isRepeated: false, + }); + }); + + it('should parse callsign with SSID', () => { + const result = Address.fromString('NOCALL-1'); + expect(result).toEqual({ + call: 'NOCALL', + ssid: '1', + isRepeated: false, + }); + }); + + it('should parse repeated address', () => { + const result = Address.fromString('WA1PLE-4*'); + expect(result).toEqual({ + call: 'WA1PLE', + ssid: '4', + isRepeated: true, + }); + }); + + it('should parse address without SSID but with repeat marker', () => { + const result = Address.fromString('WIDE1*'); + expect(result).toEqual({ + call: 'WIDE1', + ssid: '', + isRepeated: true, + }); + }); +}); + +describe('Frame.fromString', () => { + it('should parse APRS position frame (test vector 1)', () => { + const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; + const result = Frame.fromString(data); + + expect(result.source).toEqual({ + call: 'NOCALL', + ssid: '1', + isRepeated: false, + }); + + expect(result.destination).toEqual({ + call: 'APRS', + ssid: '', + isRepeated: false, + }); + + expect(result.path).toHaveLength(1); + expect(result.path[0]).toEqual({ + call: 'WIDE1', + ssid: '1', + isRepeated: false, + }); + + expect(result.payload).toBe('@092345z/:*E";qZ=OMRC/A=088132Hello World!'); + }); + + it('should parse APRS Mic-E frame with repeated digipeater (test vector 2)', () => { + const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + const result = Frame.fromString(data); + + expect(result.source).toEqual({ + call: 'N83MZ', + ssid: '', + isRepeated: false, + }); + + expect(result.destination).toEqual({ + call: 'T2TQ5U', + ssid: '', + isRepeated: false, + }); + + expect(result.path).toHaveLength(1); + expect(result.path[0]).toEqual({ + call: 'WA1PLE', + ssid: '4', + isRepeated: true, + }); + + expect(result.payload).toBe('`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'); + }); + + it('should parse frame with multiple path elements', () => { + const data = 'KB1ABC-5>APRS,WIDE1-1,WIDE2-2*,IGATE:!4903.50N/07201.75W-Test'; + const result = Frame.fromString(data); + + expect(result.source).toEqual({ + call: 'KB1ABC', + ssid: '5', + isRepeated: false, + }); + + expect(result.destination).toEqual({ + call: 'APRS', + ssid: '', + isRepeated: false, + }); + + expect(result.path).toHaveLength(3); + expect(result.path[0]).toEqual({ + call: 'WIDE1', + ssid: '1', + isRepeated: false, + }); + expect(result.path[1]).toEqual({ + call: 'WIDE2', + ssid: '2', + isRepeated: true, + }); + expect(result.path[2]).toEqual({ + call: 'IGATE', + ssid: '', + isRepeated: false, + }); + + expect(result.payload).toBe('!4903.50N/07201.75W-Test'); + }); + + it('should parse frame with no path', () => { + const data = 'W1AW>APRS::STATUS:Testing'; + const result = Frame.fromString(data); + + expect(result.source).toEqual({ + call: 'W1AW', + ssid: '', + isRepeated: false, + }); + + expect(result.destination).toEqual({ + call: 'APRS', + ssid: '', + isRepeated: false, + }); + + expect(result.path).toHaveLength(0); + expect(result.payload).toBe(':STATUS:Testing'); + }); + + it('should throw error for frame without route separator', () => { + const data = 'NOCALL-1>APRS'; + expect(() => Frame.fromString(data)).toThrow('APRS: invalid frame, no route separator found'); + }); + + it('should throw error for frame with invalid addresses', () => { + const data = 'NOCALL:payload'; + expect(() => Frame.fromString(data)).toThrow('APRS: invalid addresses in route'); + }); +}); + +describe('Frame class', () => { + it('should return a Frame instance from Frame.fromString', () => { + const data = 'W1AW>APRS:>Status message'; + const result = Frame.fromString(data); + + expect(result).toBeInstanceOf(Object); + expect(typeof result.decode).toBe('function'); + expect(typeof result.getDataTypeIdentifier).toBe('function'); + }); + + it('should get correct data type identifier for position', () => { + const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; + const frame = Frame.fromString(data); + + expect(frame.getDataTypeIdentifier()).toBe('@'); + }); + + it('should get correct data type identifier for Mic-E', () => { + const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + const frame = Frame.fromString(data); + + expect(frame.getDataTypeIdentifier()).toBe('`'); + }); + + it('should get correct data type identifier for message', () => { + const data = 'W1AW>APRS::KB1ABC-5 :Hello World'; + const frame = Frame.fromString(data); + + expect(frame.getDataTypeIdentifier()).toBe(':'); + }); + + it('should get correct data type identifier for status', () => { + const data = 'W1AW>APRS:>Status message'; + const frame = Frame.fromString(data); + + expect(frame.getDataTypeIdentifier()).toBe('>'); + }); + +it('should call decode method and return position data', () => { + const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as PositionPayload; + + // The test vector actually decodes to position data now that we've implemented it + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + }); + + it('should handle various data type identifiers in decode', () => { + const testCases = [ + { data: 'CALL>APRS:!4903.50N/07201.75W-', type: '!' }, + { data: 'CALL>APRS:=4903.50N/07201.75W-', type: '=' }, + { data: 'CALL>APRS:/092345z4903.50N/07201.75W>', type: '/' }, + { data: 'CALL>APRS:>Status Text', type: '>' }, + { data: 'CALL>APRS::ADDRESS :Message', type: ':' }, + { data: 'CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>', type: ';' }, + { data: 'CALL>APRS:)ITEM!4903.50N/07201.75W-', type: ')' }, + { data: 'CALL>APRS:?APRS?', type: '?' }, + { data: 'CALL>APRS:T#001,123,456,789', type: 'T' }, + { data: 'CALL>APRS:_10090556c...', type: '_' }, + { data: 'CALL>APRS:$GPRMC,...', type: '$' }, + { data: 'CALL>APRS:APRS:{01', type: '{' }, + { data: 'CALL>APRS:}W1AW>APRS:test', type: '}' }, + ]; + + for (const testCase of testCases) { + const frame = Frame.fromString(testCase.data); + expect(frame.getDataTypeIdentifier()).toBe(testCase.type); + // decode() returns null for now since implementations are TODO + expect(() => frame.decode()).not.toThrow(); + } + }); + + describe('Position decoding', () => { + it('should decode position with timestamp and compressed format (test vector 1)', () => { + const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as PositionPayload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.messaging).toBe(true); + expect(decoded.timestamp).toBeDefined(); + expect(decoded.timestamp?.day).toBe(9); + expect(decoded.timestamp?.hours).toBe(23); + expect(decoded.timestamp?.minutes).toBe(45); + expect(decoded.timestamp?.format).toBe('DHM'); + expect(decoded.timestamp?.zulu).toBe(true); + + expect(decoded.position).toBeDefined(); + expect(typeof decoded.position.latitude).toBe('number'); + expect(typeof decoded.position.longitude).toBe('number'); + expect(decoded.position.symbol).toBeDefined(); + expect(decoded.position.altitude).toBeCloseTo(88132 * 0.3048, 1); // feet to meters + expect(decoded.position.comment).toContain('Hello World!'); + } + }); + + it('should decode uncompressed position without timestamp', () => { + const data = 'KB1ABC>APRS:!4903.50N/07201.75W-Test message'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.messaging).toBe(false); + expect(decoded.timestamp).toBeUndefined(); + + // 49 degrees + 3.50 minutes = 49.0583 + expect(decoded.position.latitude).toBeCloseTo(49 + 3.50/60, 3); + // -72 degrees - 1.75 minutes = -72.0292 + expect(decoded.position.longitude).toBeCloseTo(-(72 + 1.75/60), 3); + expect(decoded.position.symbol?.table).toBe('/'); + expect(decoded.position.symbol?.code).toBe('-'); + expect(decoded.position.comment).toBe('Test message'); + } + }); + + it('should decode uncompressed position with alternate symbol table (\\)', () => { + const data = String.raw`W1AW>APRS:!4903.50N\07201.75W-Test message`; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.symbol?.table).toBe('\\'); + expect(decoded.position.comment).toBe('Test message'); + } + }); + it('should decode position with messaging capability', () => { + const data = 'W1AW>APRS:=4903.50N/07201.75W-'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.messaging).toBe(true); + expect(decoded.timestamp).toBeUndefined(); + } + }); + + it('should decode position with timestamp (HMS format)', () => { + const data = 'CALL>APRS:/234517h4903.50N/07201.75W>'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.messaging).toBe(false); + expect(decoded.timestamp).toBeDefined(); + expect(decoded.timestamp?.hours).toBe(23); + expect(decoded.timestamp?.minutes).toBe(45); + expect(decoded.timestamp?.seconds).toBe(17); + expect(decoded.timestamp?.format).toBe('HMS'); + } + }); + + it('should extract altitude from comment', () => { + const data = 'CALL>APRS:!4903.50N/07201.75W>/A=001234'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1); + } + }); + + it('should handle southern and western hemispheres', () => { + const data = 'CALL>APRS:!3345.67S/15112.34E-'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // 33 degrees + 45.67 minutes = 33.7612 degrees South = -33.7612 + expect(decoded.position.latitude).toBeLessThan(0); + expect(decoded.position.longitude).toBeGreaterThan(0); + expect(decoded.position.latitude).toBeCloseTo(-(33 + 45.67/60), 3); + // 151 degrees + 12.34 minutes = 151.2057 degrees East + expect(decoded.position.longitude).toBeCloseTo(151 + 12.34/60, 3); + } + }); + + it('should return null for invalid position data', () => { + const data = 'CALL>APRS:!invalid'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).toBeNull(); + }); + + it('should handle position with ambiguity level 1 (1 space)', () => { + // Spaces mask the rightmost digits for privacy + // 4903.5 means ambiguity level 1 (±0.05 minute) + const data = 'CALL>APRS:!4903.5 N/07201.75W-'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.ambiguity).toBe(1); + // Spaces are replaced with 0 for parsing, so 4903.5 becomes 4903.50 + expect(decoded.position.latitude).toBeCloseTo(49 + 3.5/60, 3); + } + }); + + it('should handle position with ambiguity level 2 (2 spaces)', () => { + // 4903. means ambiguity level 2 (±0.5 minutes) - spaces replace last 2 decimal digits + const data = 'CALL>APRS:!4903. N/07201.75W-'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.ambiguity).toBe(2); + expect(decoded.position.latitude).toBeCloseTo(49 + 3/60, 3); + } + }); + + it('should handle position with ambiguity level 1 in both lat and lon', () => { + // Both lat and lon have 1 space for ambiguity 1 (±0.05 minute) + const data = 'CALL>APRS:!4903.5 N/07201.7 W-'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.ambiguity).toBe(1); + expect(decoded.position.latitude).toBeCloseTo(49 + 3.5/60, 3); + expect(decoded.position.longitude).toBeCloseTo(-(72 + 1.7/60), 3); + } + }); + + it('should handle position with ambiguity level 2 in both lat and lon', () => { + // Both lat and lon have 2 spaces for ambiguity 2 (±0.5 minutes) + const data = 'CALL>APRS:!4903. N/07201. W-'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.ambiguity).toBe(2); + expect(decoded.position.latitude).toBeCloseTo(49 + 3/60, 3); + expect(decoded.position.longitude).toBeCloseTo(-(72 + 1/60), 3); + } + }); + + it('should have toDate method on timestamp', () => { + const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position' && decoded.timestamp) { + expect(typeof decoded.timestamp.toDate).toBe('function'); + const date = decoded.timestamp.toDate(); + expect(date).toBeInstanceOf(Date); + expect(date.getUTCDate()).toBe(9); + expect(date.getUTCHours()).toBe(23); + expect(date.getUTCMinutes()).toBe(45); + } + }); + }); + + describe('Object decoding', () => { + it('should decode object payload with uncompressed position', () => { + const data = 'CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('object'); + + if (decoded && decoded.type === 'object') { + expect(decoded.name).toBe('OBJECT'); + expect(decoded.alive).toBe(true); + expect(decoded.timestamp).toBeDefined(); + expect(decoded.position).toBeDefined(); + expect(typeof decoded.position.latitude).toBe('number'); + expect(typeof decoded.position.longitude).toBe('number'); + expect(decoded.position.comment).toBe('Test object'); + } + }); + + it('should emit object sections when emitSections is true', () => { + const data = 'CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object'; + const frame = Frame.fromString(data); + const result = frame.decode(true) as { payload: Payload | null; structure: PacketStructure }; + + expect(result.payload).not.toBeNull(); + expect(result.payload?.type).toBe('object'); + expect(result.structure.length).toBeGreaterThan(0); + + const nameSection = result.structure.find((s) => s.name === 'object name'); + expect(nameSection).toBeDefined(); + const stateSection = result.structure.find((s) => s.name === 'object state'); + expect(stateSection).toBeDefined(); + const timestampSection = result.structure.find((s) => s.name === 'timestamp'); + expect(timestampSection).toBeDefined(); + const positionSection = result.structure.find((s) => s.name === 'position'); + expect(positionSection).toBeDefined(); + }); + }); +}); + +describe('Timestamp class', () => { + it('should create DHM timestamp and convert to Date', () => { + const ts = new Timestamp(14, 30, 'DHM', { day: 15, zulu: true }); + + expect(ts.hours).toBe(14); + expect(ts.minutes).toBe(30); + expect(ts.day).toBe(15); + expect(ts.format).toBe('DHM'); + expect(ts.zulu).toBe(true); + + const date = ts.toDate(); + expect(date).toBeInstanceOf(Date); + expect(date.getUTCDate()).toBe(15); + expect(date.getUTCHours()).toBe(14); + expect(date.getUTCMinutes()).toBe(30); + }); + + it('should create HMS timestamp and convert to Date', () => { + const ts = new Timestamp(12, 45, 'HMS', { seconds: 30, zulu: true }); + + expect(ts.hours).toBe(12); + expect(ts.minutes).toBe(45); + expect(ts.seconds).toBe(30); + expect(ts.format).toBe('HMS'); + + const date = ts.toDate(); + expect(date).toBeInstanceOf(Date); + expect(date.getUTCHours()).toBe(12); + expect(date.getUTCMinutes()).toBe(45); + expect(date.getUTCSeconds()).toBe(30); + }); + + it('should create MDHM timestamp and convert to Date', () => { + const ts = new Timestamp(16, 20, 'MDHM', { month: 3, day: 5, zulu: false }); + + expect(ts.hours).toBe(16); + expect(ts.minutes).toBe(20); + expect(ts.month).toBe(3); + expect(ts.day).toBe(5); + expect(ts.format).toBe('MDHM'); + expect(ts.zulu).toBe(false); + + const date = ts.toDate(); + expect(date).toBeInstanceOf(Date); + expect(date.getMonth()).toBe(2); // 0-indexed + expect(date.getDate()).toBe(5); + expect(date.getHours()).toBe(16); + expect(date.getMinutes()).toBe(20); + }); + + it('should handle DHM timestamp that is in the future (use previous month)', () => { + const now = new Date(); + const futureDay = now.getUTCDate() + 5; + + const ts = new Timestamp(12, 0, 'DHM', { day: futureDay, zulu: true }); + const date = ts.toDate(); + + // Should be in the past or very close to now + expect(date <= now).toBe(true); + }); + + it('should handle HMS timestamp that is in the future (use yesterday)', () => { + const now = new Date(); + const futureHours = now.getUTCHours() + 2; + + if (futureHours < 24) { + const ts = new Timestamp(futureHours, 0, 'HMS', { seconds: 0, zulu: true }); + const date = ts.toDate(); + + // Should be in the past + expect(date <= now).toBe(true); + } + }); + + it('should handle MDHM timestamp that is in the future (use last year)', () => { + const now = new Date(); + const futureMonth = now.getMonth() + 2; + + if (futureMonth < 12) { + const ts = new Timestamp(12, 0, 'MDHM', { + month: futureMonth + 1, + day: 1, + zulu: false + }); + const date = ts.toDate(); + + // Should be in the past + expect(date <= now).toBe(true); + } + }); +}); + +describe('Mic-E decoding', () => { + describe('Basic Mic-E frames', () => { + it('should decode a basic Mic-E packet (current format)', () => { + // Destination: T2TQ5U encodes latitude ~42.3456N + // T=1, 2=2, T=1, Q=1, 5=5, U=1 (digits for latitude) + // Information field encodes longitude, speed, course, and symbols + const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.messaging).toBe(true); + expect(decoded.position).toBeDefined(); + expect(typeof decoded.position.latitude).toBe('number'); + expect(typeof decoded.position.longitude).toBe('number'); + expect(decoded.position.symbol).toBeDefined(); + expect(decoded.micE).toBeDefined(); + expect(decoded.micE?.messageType).toBeDefined(); + } + }); + + it('should decode a Mic-E packet with old format (single quote)', () => { + // Similar to above but with single quote (') data type identifier for old Mic-E + const data = 'CALL>T2TQ5U:\'c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + }); + }); + + describe('Latitude decoding from destination', () => { + it('should decode latitude from numeric digits (0-9)', () => { + // Destination: 123456 -> 12°34.56'N with specific message bits + const data = 'CALL>123456:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // 12 degrees + 34.56 minutes = 12.576 degrees + expect(decoded.position.latitude).toBeCloseTo(12 + 34.56/60, 3); + } + }); + + it('should decode latitude from letter digits (A-J)', () => { + // A=0, B=1, C=2, D=3, E=4, F=5, G=6, H=7, I=8, J=9 + // ABC0EF -> 012045 -> 01°20.45' (using 0 at position 3 for North) + const data = 'CALL>ABC0EF:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // 01 degrees + 20.45 minutes = 1.340833 degrees North + expect(decoded.position.latitude).toBeCloseTo(1 + 20.45/60, 3); + } + }); + + it('should decode latitude with mixed digits and letters', () => { + // 4AB2DE -> 401234 -> 40°12.34'N (using 2 at position 3 for North) + const data = 'CALL>4AB2DE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // 40 degrees + 12.34 minutes + expect(decoded.position.latitude).toBeCloseTo(40 + 12.34/60, 3); + } + }); + + it('should decode latitude for southern hemisphere', () => { + // When messageBits[3] == 1, it's southern hemisphere + // For 'P' char at position 3 (msgBit=1 for south), combined appropriately + const data = 'CALL>4A0P0U:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Should be negative for south + expect(decoded.position.latitude).toBeLessThan(0); + } + }); + }); + + describe('Longitude decoding from information field', () => { + it('should decode longitude from information field', () => { + // Mic-E info field bytes encode longitude degrees, minutes, hundredths + // Testing with constructed values + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(typeof decoded.position.longitude).toBe('number'); + // Longitude should be within valid range + expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180); + expect(decoded.position.longitude).toBeLessThanOrEqual(180); + } + }); + + it('should handle eastern hemisphere longitude', () => { + // When messageBits[4] == 1, it's eastern hemisphere (positive) + // Need specific destination encoding for this + const data = 'CALL>4ABPDE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Eastern hemisphere could be positive (depends on other flags) + expect(typeof decoded.position.longitude).toBe('number'); + } + }); + + it('should handle longitude offset +100', () => { + // When messageBits[5] == 1, add 100 to longitude degrees + // Need 'P' or similar at position 5 + const data = 'CALL>4ABCDP:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(typeof decoded.position.longitude).toBe('number'); + // Longitude offset should be applied (100+ degrees) + expect(Math.abs(decoded.position.longitude)).toBeGreaterThan(90); + } + }); + }); + + describe('Speed and course decoding', () => { + it('should decode speed from information field', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}Speed test'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Speed might be 0 or present + if (decoded.position.speed !== undefined) { + expect(decoded.position.speed).toBeGreaterThanOrEqual(0); + // Speed should be in km/h + expect(typeof decoded.position.speed).toBe('number'); + } + } + }); + + it('should decode course from information field', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Course might be 0 or present + if (decoded.position.course !== undefined) { + expect(decoded.position.course).toBeGreaterThanOrEqual(0); + expect(decoded.position.course).toBeLessThan(360); + } + } + }); + + it('should not include zero speed in result', () => { + // When speed is 0, it should not be included in the result + const data = 'CALL>4ABCDE:`\x1c\x1c\x1c\x1c\x1c\x1c/>}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Speed of 0 should not be set + expect(decoded.position.speed).toBeUndefined(); + } + }); + + it('should not include zero or 360+ course in result', () => { + const data = 'CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Course of 0 or >= 360 should not be set + if (decoded.position.course !== undefined) { + expect(decoded.position.course).toBeGreaterThan(0); + expect(decoded.position.course).toBeLessThan(360); + } + } + }); + }); + + describe('Symbol decoding', () => { + it('should decode symbol table and code', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.symbol).toBeDefined(); + expect(decoded.position.symbol?.table).toBeDefined(); + expect(decoded.position.symbol?.code).toBeDefined(); + expect(typeof decoded.position.symbol?.table).toBe('string'); + expect(typeof decoded.position.symbol?.code).toBe('string'); + } + }); + }); + + describe('Altitude decoding', () => { + it('should decode altitude from /A=NNNNNN format', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}/A=001234'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // 1234 feet = 1234 * 0.3048 meters + expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1); + } + }); + + it('should decode altitude from base-91 format }abc', () => { + // Base-91 altitude format: }xyz where xyz is base-91 encoded altitude + // The altitude encoding is (altitude in feet + 10000) in base-91 + const data = 'CALL>4AB2DE:`c.l+@&\'/\'"G:}}S^X'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Base-91 altitude should be decoded (using valid base-91 characters) + // Even if altitude calculation results in negative value, it should be defined + if (decoded.position.comment?.startsWith('}')) { + // Altitude should be extracted from base-91 format + expect(decoded.position.altitude).toBeDefined(); + } + } + }); + + it('should prefer /A= format over base-91 when both present', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}}!!!/A=005000'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Should use /A= format (5000 feet) + expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1); + } + }); + + it('should handle comment without altitude', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}Just a comment'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.altitude).toBeUndefined(); + expect(decoded.position.comment).toContain('Just a comment'); + } + }); + }); + + describe('Message type decoding', () => { + it('should decode message type M0 (Off Duty)', () => { + // Message bits 0,1,2 = 0,0,0 -> M0: Off Duty + // Use digits 0-9 for message bit 0 + const data = 'CALL>012345:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.micE?.messageType).toBe('M0: Off Duty'); + } + }); + + it('should decode message type M7 (Emergency)', () => { + // Message bits 0,1,2 = 1,1,1 -> M7: Emergency + // Use letters A-J for message bit 1 + const data = 'CALL>ABCDEF:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Should contain a message type + expect(decoded.micE?.messageType).toBeDefined(); + expect(typeof decoded.micE?.messageType).toBe('string'); + } + }); + + it('should decode standard vs custom message indicator', () => { + const data = 'CALL>ABCDEF:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.micE?.isStandard).toBeDefined(); + expect(typeof decoded.micE?.isStandard).toBe('boolean'); + } + }); + }); + + describe('Comment and telemetry', () => { + it('should extract comment from remaining data', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}This is a test comment'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + expect(decoded.position.comment).toContain('This is a test comment'); + } + }); + + it('should handle empty comment', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Empty comment should still be defined but empty + expect(decoded.position.comment).toBeDefined(); + } + }); + }); + + describe('Error handling', () => { + it('should return null for destination address too short', () => { + const data = 'CALL>SHORT:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + // Should fail because destination is too short + expect(decoded).toBeNull(); + }); + + it('should return null for payload too short', () => { + const data = 'CALL>4ABCDE:`short'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + // Should fail because payload is too short (needs at least 9 bytes with data type) + expect(decoded).toBeNull(); + }); + + it('should return null for invalid destination characters', () => { + const data = 'CALL>4@BC#E:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + // Should fail because destination contains invalid characters + expect(decoded).toBeNull(); + }); + + it('should handle exceptions gracefully', () => { + // Malformed data that might cause exceptions + const data = 'CALL>4ABCDE:`\x00\x00\x00\x00\x00\x00\x00\x00'; + const frame = Frame.fromString(data); + + // Should not throw, just return null + expect(() => frame.decode()).not.toThrow(); + const decoded = frame.decode() as Payload; + // Might be null or might decode with weird values + expect(decoded === null || decoded?.type === 'position').toBe(true); + }); + }); + + describe('Real-world test vectors', () => { + it('should decode real Mic-E packet from test vector 2', () => { + // From the existing test: N83MZ>T2TQ5U with specific encoding + const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + expect(decoded?.type).toBe('position'); + + if (decoded && decoded.type === 'position') { + expect(decoded.messaging).toBe(true); + expect(decoded.position.latitude).toBeDefined(); + expect(decoded.position.longitude).toBeDefined(); + expect(decoded.position.symbol).toBeDefined(); + expect(decoded.micE).toBeDefined(); + + // Verify reasonable coordinate ranges + expect(Math.abs(decoded.position.latitude)).toBeLessThanOrEqual(90); + expect(Math.abs(decoded.position.longitude)).toBeLessThanOrEqual(180); + } + }); + }); + + describe('Messaging capability', () => { + it('should always set messaging to true for Mic-E', () => { + const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + const frame = Frame.fromString(data); + const decoded = frame.decode() as Payload; + + expect(decoded).not.toBeNull(); + + if (decoded && decoded.type === 'position') { + // Mic-E always has messaging capability + expect(decoded.messaging).toBe(true); + } + }); + }); +}); + +describe('Packet dissection with sections', () => { + it('should emit routing sections when emitSections is true', () => { + const data = 'KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test'; + const frame = Frame.fromString(data); + const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + + expect(result).toHaveProperty('payload'); + expect(result).toHaveProperty('structure'); + expect(result.structure).toBeDefined(); + expect(result.structure.length).toBeGreaterThan(0); + + // Check routing section (capital R as implemented) + const routingSection = result.structure.find(s => s.name === 'Routing'); + expect(routingSection).toBeDefined(); + expect(routingSection?.fields).toBeDefined(); + expect(routingSection?.fields?.length).toBeGreaterThan(0); + + // Check for source and destination fields + const sourceField = routingSection?.fields?.find(a => a.name === 'Source address'); + expect(sourceField).toBeDefined(); + expect(sourceField?.size).toBeGreaterThan(0); + + const destField = routingSection?.fields?.find(a => a.name === 'Destination address'); + expect(destField).toBeDefined(); + expect(destField?.size).toBeGreaterThan(0); + }); + + it('should emit position payload sections when emitSections is true', () => { + const data = 'CALL>APRS:!4903.50N/07201.75W-Test message'; + const frame = Frame.fromString(data); + const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + + expect(result.payload).not.toBeNull(); + expect(result.payload?.type).toBe('position'); + + // Check if result has sections at top level + expect(result.structure).toBeDefined(); + expect(result.structure?.length).toBeGreaterThan(0); + + // Find position section + const positionSection = result.structure?.find(s => s.name === 'position'); + expect(positionSection).toBeDefined(); + expect(positionSection?.data.length).toBe(19); // Uncompressed position is 19 bytes + expect(positionSection?.fields).toBeDefined(); + expect(positionSection?.fields?.length).toBeGreaterThan(0); + }); + + it('should not emit sections when emitSections is false or omitted', () => { + const data = 'CALL>APRS:!4903.50N/07201.75W-Test'; + const frame = Frame.fromString(data); + const result = frame.decode() as Payload; + + // Result should be just the DecodedPayload, not an object with payload and sections + expect(result).not.toBeNull(); + expect(result?.type).toBe('position'); + expect((result as any).sections).toBeUndefined(); + }); + + it('should emit timestamp section when present', () => { + const data = 'CALL>APRS:@092345z4903.50N/07201.75W>'; + const frame = Frame.fromString(data); + const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + + expect(result.payload?.type).toBe('position'); + + const timestampSection = result.structure?.find(s => s.name === 'timestamp'); + expect(timestampSection).toBeDefined(); + expect(timestampSection?.data.length).toBe(7); + expect(timestampSection?.fields?.map(a => a.name)).toEqual([ + 'day (DD)', + 'hour (HH)', + 'minute (MM)', + 'timezone indicator', + ]); + }); + + it('should emit compressed position sections', () => { + const data = 'NOCALL-1>APRS:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; + const frame = Frame.fromString(data); + const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + + expect(result.payload?.type).toBe('position'); + + const positionSection = result.structure?.find(s => s.name === 'position'); + expect(positionSection).toBeDefined(); + expect(positionSection?.data.length).toBe(13); // Compressed position is 13 bytes + + // Check for base91 encoded attributes + const latAttr = positionSection?.fields?.find(a => a.name === 'latitude'); + expect(latAttr).toBeDefined(); + expect(latAttr?.type).toBe(FieldType.STRING); + }); + + it('should emit comment section', () => { + const data = 'CALL>APRS:!4903.50N/07201.75W-Test message'; + const frame = Frame.fromString(data); + const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + + expect(result.payload?.type).toBe('position'); + const commentSection = result.structure?.find(s => s.name === 'comment'); + expect(commentSection).toBeDefined(); + expect(commentSection?.data.length).toBe('Test message'.length); + expect(commentSection?.fields?.[0]?.name).toBe('text'); + }); +}); diff --git a/test/parser.test.ts b/test/parser.test.ts new file mode 100644 index 0000000..000c3f5 --- /dev/null +++ b/test/parser.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from 'vitest'; +import { + base91ToNumber, + knotsToKmh, + kmhToKnots, + feetToMeters, + metersToFeet, + celsiusToFahrenheit, + fahrenheitToCelsius, +} from '../src/parser'; + +describe('parser utilities', () => { + describe('base91ToNumber', () => { + it('decodes all-! to 0', () => { + expect(base91ToNumber('!!!!')).toBe(0); + }); + + it('decodes single character correctly', () => { + // 'A' === 65, digit = 65 - 33 = 32 + expect(base91ToNumber('A')).toBe(32); + }); + + it('should decode multiple Base91 characters', () => { + // "!!" = 0 * 91 + 0 = 0 + expect(base91ToNumber('!!')).toBe(0); + + // "!#" = 0 * 91 + 2 = 2 + expect(base91ToNumber('!#')).toBe(2); + + // "#!" = 2 * 91 + 0 = 182 + expect(base91ToNumber('#!')).toBe(182); + + // "##" = 2 * 91 + 2 = 184 + expect(base91ToNumber('##')).toBe(184); + }); + + it('should decode 4-character Base91 strings (used in APRS)', () => { + // Test with printable ASCII Base91 characters (33-123) + const testValue = base91ToNumber('!#%\''); + expect(testValue).toBeGreaterThan(0); + expect(testValue).toBeLessThan(91 * 91 * 91 * 91); + }); + + it('should decode maximum valid Base91 value', () => { + // Maximum is '{' (ASCII 123, digit 90) repeated + const maxValue = base91ToNumber('{{{{'); + const expected = 90 * 91 * 91 * 91 + 90 * 91 * 91 + 90 * 91 + 90; + expect(maxValue).toBe(expected); + }); + + it('should handle APRS compressed position example', () => { + // Using actual characters from APRS test vector + const latStr = '/:*E'; + const lonStr = 'qZ=O'; + + const latValue = base91ToNumber(latStr); + const lonValue = base91ToNumber(lonStr); + + // Just verify they decode without error and produce valid numbers + expect(typeof latValue).toBe('number'); + expect(typeof lonValue).toBe('number'); + expect(latValue).toBeGreaterThanOrEqual(0); + expect(lonValue).toBeGreaterThanOrEqual(0); + }); + + it('throws on invalid character', () => { + expect(() => base91ToNumber(' ')).toThrow(); // space (code 32) is invalid + }); + }); + + describe('unit conversions', () => { + it('converts knots <-> km/h', () => { + expect(knotsToKmh(10)).toBeCloseTo(18.52, 5); + expect(kmhToKnots(18.52)).toBeCloseTo(10, 3); + }); + + it('converts feet <-> meters', () => { + expect(feetToMeters(10)).toBeCloseTo(3.048, 6); + expect(metersToFeet(3.048)).toBeCloseTo(10, 6); + }); + + it('converts celsius <-> fahrenheit', () => { + expect(celsiusToFahrenheit(0)).toBeCloseTo(32, 6); + expect(fahrenheitToCelsius(32)).toBeCloseTo(0, 6); + expect(celsiusToFahrenheit(100)).toBeCloseTo(212, 6); + expect(fahrenheitToCelsius(212)).toBeCloseTo(100, 6); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9f12c56 --- /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", "test/crypto.test.ts", "test/identity.test.ts", "test/packet.test.ts", "test/parser.test.ts"], + "exclude": ["node_modules", "dist", "test"] +}