diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..067b644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build, Typecheck, Lint, Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6 + with: + package_json_file: package.json + + - uses: actions/setup-node@v7 + with: + node-version-file: .tool-versions + cache: pnpm + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Compile the vendored parser + run: pnpm run vendor:build + + - name: Build the library (dual ESM/CJS, verified with attw) + run: pnpm run build + + - name: Typecheck + run: pnpm run typecheck + + - name: Lint + run: pnpm run lint + + - name: Round-trip wire-mesh's conformance vectors through the generated schemas + run: pnpm run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da894c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.turbo/ +.eslintcache +coverage/ +test/fixtures/generated/protocol.ts diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..d0b4db8 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 26 diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..db9aa98 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,35 @@ +import { exadevConfig } from "@exadev/eslint-config"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import globals from "globals"; + +export default exadevConfig( + {}, + { + ignores: [ + "dist", + "coverage", + "node_modules", + ".turbo", + "vendor", + "test/fixtures/generated", + ], + }, + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.node.json"], + tsconfigRootDir: import.meta.dirname, + }, + globals: { ...globals.node }, + }, + }, + { + rules: { + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports" }, + ], + }, + }, + eslintPluginPrettierRecommended, +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..f1321e7 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "cddl.js", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.33.0", + "files": [ + "dist" + ], + "scripts": { + "build": "turbo run _build", + "_build": "tsdown", + "vendor:build": "turbo run _vendor:build", + "_vendor:build": "tsc -p vendor/cddl/tsconfig.json", + "test": "turbo run _test", + "_test": "vitest run", + "typecheck": "turbo run _typecheck", + "_typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.node.json --noEmit && tsc -p vendor/cddl/tsconfig.json --noEmit", + "lint": "turbo run _lint", + "_lint": "eslint . --fix --cache --max-warnings 0" + }, + "dependencies": { + "camelcase": "9.0.0", + "cbor2": "2.3.0", + "zod": "4.5.4" + }, + "devDependencies": { + "@arethetypeswrong/cli": "0.18.5", + "@exadev/eslint-config": "2.10.6", + "@types/node": "26.4.1", + "eslint": "10.10.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "globals": "17.12.0", + "jiti": "2.7.0", + "prettier": "3.9.6", + "tsdown": "0.23.0", + "turbo": "2.10.12", + "typescript": "6.0.3", + "vitest": "5.0.0" + }, + "exports": { + "./emitter": { + "import": "./dist/emitter.mjs", + "require": "./dist/emitter.cjs" + }, + "./runtime": { + "import": "./dist/runtime.mjs", + "require": "./dist/runtime.cjs" + }, + "./package.json": "./package.json" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..536b921 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2563 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + camelcase: + specifier: 9.0.0 + version: 9.0.0 + cbor2: + specifier: 2.3.0 + version: 2.3.0 + zod: + specifier: 4.5.4 + version: 4.5.4 + devDependencies: + '@arethetypeswrong/cli': + specifier: 0.18.5 + version: 0.18.5 + '@exadev/eslint-config': + specifier: 2.10.6 + version: 2.10.6(eslint@10.10.0(jiti@2.7.0))(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(typescript@6.0.3) + '@types/node': + specifier: 26.4.1 + version: 26.4.1 + eslint: + specifier: 10.10.0 + version: 10.10.0(jiti@2.7.0) + eslint-config-prettier: + specifier: 10.1.8 + version: 10.1.8(eslint@10.10.0(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: 5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)))(eslint@10.10.0(jiti@2.7.0))(prettier@3.9.6) + globals: + specifier: 17.12.0 + version: 17.12.0 + jiti: + specifier: 2.7.0 + version: 2.7.0 + prettier: + specifier: 3.9.6 + version: 3.9.6 + tsdown: + specifier: 0.23.0 + version: 0.23.0(@arethetypeswrong/core@0.18.5)(typescript@6.0.3) + turbo: + specifier: 2.10.12 + version: 2.10.12 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 5.0.0 + version: 5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + +packages: + + '@andrewbranch/untar.js@1.0.4': + resolution: {integrity: sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==} + + '@arethetypeswrong/cli@0.18.5': + resolution: {integrity: sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==} + engines: {node: '>=20'} + hasBin: true + + '@arethetypeswrong/core@0.18.5': + resolution: {integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==} + engines: {node: '>=20'} + + '@braidai/lang@1.1.2': + resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cto.af/wtf8@0.0.5': + resolution: {integrity: sha512-LfUFi+Vv4eDzj+XAtR89e3wwjXA/NZjUSwU5NhwbBrLecxPaBYFy3exCuc1j+D4UZeOVdqlsl8G7LmOt18V0tg==} + engines: {node: '>=20'} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exadev/eslint-config@2.10.6': + resolution: {integrity: sha512-iU/0uHNqo9GLLpLzlBA9KN3233aKHOj7bQYsc4in1HCZaUHqjNmAdX8bPs99SxhNjsP7wOpBl7Inrh6w3JDlqw==} + engines: {node: '>=20'} + peerDependencies: + '@next/eslint-plugin-next': ^16.3.2 + eslint: '>=10.0.0' + eslint-plugin-jsx-a11y: ^6.10.2 + eslint-plugin-react: ^7.37.5 + eslint-plugin-react-hooks: ^7.1.1 + typescript: '>=4.8.4' + typescript-eslint: '>=8.0.0' + peerDependenciesMeta: + '@next/eslint-plugin-next': + optional: true + eslint-plugin-jsx-a11y: + optional: true + eslint-plugin-react: + optional: true + eslint-plugin-react-hooks: + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@loaderkit/resolve@1.0.6': + resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@turbo/darwin-64@2.10.12': + resolution: {integrity: sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.10.12': + resolution: {integrity: sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.10.12': + resolution: {integrity: sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==} + cpu: [x64] + os: [android, linux] + + '@turbo/linux-arm64@2.10.12': + resolution: {integrity: sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==} + cpu: [arm64] + os: [android, linux] + + '@turbo/windows-64@2.10.12': + resolution: {integrity: sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.10.12': + resolution: {integrity: sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==} + cpu: [arm64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} + + '@typescript-eslint/eslint-plugin@8.69.0': + resolution: {integrity: sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.69.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.69.0': + resolution: {integrity: sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.69.0': + resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.69.0': + resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.69.0': + resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.69.0': + resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.69.0': + resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.69.0': + resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.69.0': + resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.69.0': + resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/mocker@5.0.0': + resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.0': + resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} + + '@yuku-codegen/binding-android-arm64@0.9.3': + resolution: {integrity: sha512-viote6xAyL5cKLquV2X2wRfopSckH+msDYbaI8Hh8JAaogYs8MJZVRUbSrbsY29TaPrIFZwNRwQ8+YSxs0dkGw==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.9.3': + resolution: {integrity: sha512-y2PGOLyxc724EJ+Et/5PxGfutQuV1Z2J9cxHo6W1I5CR3nk0i03J4yPrnw6rNJOfrtlISzcc7q0RrSyfPndpIg==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.9.3': + resolution: {integrity: sha512-xZ9UpXUOLmsrKVUp7MRXxWU3drNiilRC42OLpjWEhnOehIGVF1bZgzHcqRYJxVyU53RMrkRMsaxplkGd6Qo/ow==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.9.3': + resolution: {integrity: sha512-RGCYSZw3VonreVTpur9iOfnbMngR2/f7UOE7gwcDx5WoHjIhtmTK9EIq9qs78ARdMFAPzKp5OzHljl5QMaGJ7g==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.9.3': + resolution: {integrity: sha512-kjIJIw39GSPTF0hnq+jnM0tR+J5WlariAAWetxMtawNskITDT1ZigaK3YF5hMhDz2mAofaM1t+63qtx+7aXjeg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.9.3': + resolution: {integrity: sha512-TVHeVdzaS4ub86URrQufiy4t01ZtdyKDZ5sTPqWFKAUbQJ7rQ0o5vIT+/jCeHQbP+Yf9p5peRdmPbcZewoDNiA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.3': + resolution: {integrity: sha512-9aQeeLh1qaCa2GcyzHnwLKGfFrsI+KOyhnh4+fICSq5kyhzCWQfXVCCK4RTEZPLcyuVwfN5Id0JEYavRN4oNTQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.9.3': + resolution: {integrity: sha512-TLXA5Hd1nr8VSP2MtxcFddsBIHj+DPpyG5eberEGMATndgG7STlKQmle51MV0MZ8UgsdYM6CybIVpzlCPQwP9w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-x64-gnu@0.9.3': + resolution: {integrity: sha512-tk0BFbF3Clb9k9biPH3qmr+Qwk24rRM26+HY91hYXmZlzlepZG0scQ6OffZFWtzwKE5JjlZpMdcWj1lTiHbEjA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-x64-musl@0.9.3': + resolution: {integrity: sha512-xo+pXshsCXruEEkDMbwHqkeTyC4XHb6A6oXr6x2YK3jOMcS5kZz0e26BmTCr40J0nY/K3ysmwG9R1xHygtiuwQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-win32-arm64@0.9.3': + resolution: {integrity: sha512-70gAQo6HZzMgIJTjeMZOEO2XaRj4GcNGP/n81R9tXQv0x8d4ZHnZDv+ygeWfHo+xchAUPwpnrVZA4p6RhJa3GQ==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.9.3': + resolution: {integrity: sha512-7Hz3NGlq6qBBR8zMEk6GMZivofNjnYZpMXSoUwUgz9Ur2LaivuP23HQh0ern+T6HVkTJEvilw4VJrg1rX1Ugcg==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.9.3': + resolution: {integrity: sha512-z3tDGTaUXD5Q4IuebFOY/QHxhR/SqujMhkMv9C5bh25upyFEXdafp0MsXQIIheWhVZzn5VMOniyxFni/7s9LgQ==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.9.3': + resolution: {integrity: sha512-hzKvKSKS7z3ufnu1VkQYEoxmS6A5uNnkwukKnc2atxpWdq648nLVOqut4h1jUXjbM2WcoTfuZLF6AHAGFf8cmg==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.9.3': + resolution: {integrity: sha512-OM2PiVlPATvzlj/KGHNlu+t8FC3YzM5joVNjhsz/EaigQ8x2UUQ1Q0agQLiv5GZ2L8TSzrLUwBCZ7YOvP8q+tw==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.9.3': + resolution: {integrity: sha512-WMn9M4LHNVysGNwsAJ9gpRPqQIW2lcPrDNGcyk0agx3IYqCJq1MQugh6Be8Otc5U08eGdE39o8Kx9YWJmXz7GA==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.9.3': + resolution: {integrity: sha512-vqKjwiyW1FWvbykYMEQIcAJwA17WxK3rxxqDuyCvQwcNVaLEUxI4BXiUdlF3kHucc7MnoFQhoRPkySgOzlLM+Q==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.9.3': + resolution: {integrity: sha512-qohilhYOT+zkt2gYzym4F1T6BzRdvPqS9/sFB03pmnVV8LrvjOXVsGwEBAa3CEvzJKhAZjdTmVz7zsVdjyHWLg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.9.3': + resolution: {integrity: sha512-tvTIyUvTGkeee68i4JIo9o27As+Ug6LXOZvElGgFbTs6+KBdb5LGhvugaIQljdfIsm2XnIbOLG6OnQSXHMLB9w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.9.3': + resolution: {integrity: sha512-OWZBHW1wuChBpFlrF+On1yiBcFPMRrj2g0VKsM/PCBGffu1OM0ORkS+vLS3Snu6wYwndM5Dd9Ctvux6eUlrQjA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.9.3': + resolution: {integrity: sha512-/tbk1h0dlADOCngbiQO9V3SHwBIJkoGBq8BDEraSw2CC3nGxPEPTCCYUDp5738Ij2FxVwy1FWVuhFkHvpMrPbQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.9.3': + resolution: {integrity: sha512-u3+0sCso/mcjDvtc3866D2giV7l34PDyDVBkMesAWa3DWbIWPlybehi2SSnmScgArV22ctqSSgUzdBBVJ6zYAg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.9.3': + resolution: {integrity: sha512-xnGEvdhyjRkXozHtpXVpEEkyGZWAxJ3RoILv7XRV5SvTEztaTCk5GQyfcOzI9An/EJtcL4ERCI8QmS0TpDPJiA==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.9.3': + resolution: {integrity: sha512-N5eShGcuwnrXEprePFmEjtng6acZmtnC4zgazK9xxkavcx1q1uX8Nz8lU5RS973EMlNr902cIc6Cd2D4tqZDBg==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.9.3': + resolution: {integrity: sha512-rFE+5P4g2wxko5C85MugJOlVjBHEQq87dIkhLkniLXLp63PEtgaFjD954i5HXlfnyzLxPcZHsSOVDVgmo1HToA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + camelcase@9.0.0: + resolution: {integrity: sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==} + engines: {node: '>=20'} + + cbor2@2.3.0: + resolution: {integrity: sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==} + engines: {node: '>=20'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.12.0: + resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.8: + resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} + engines: {node: '>= 4'} + + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + + marked-terminal@7.3.0: + resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} + engines: {node: '>=16.0.0'} + peerDependencies: + marked: '>=1 <16' + + marked@9.1.6: + resolution: {integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==} + engines: {node: '>= 16'} + hasBin: true + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.28.5: + resolution: {integrity: sha512-yYd3C9CeJwqjOc9X23m0Tyxcqic491uLZlfg51szT287S8zCCqLR2uoySoElgqy2CLn7PdXcEo1dlkBs4n1WHg==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + rolldown: ^1.2.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsdown@0.23.0: + resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.23.0 + '@tsdown/exe': 0.23.0 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + + turbo@2.10.12: + resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.69.0: + resolution: {integrity: sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.6.1-rc: + resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + verkit@0.4.0: + resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} + engines: {node: '>=18.12.0'} + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.0: + resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.0 + '@vitest/browser-preview': 5.0.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.0 + '@vitest/coverage-v8': 5.0.0 + '@vitest/ui': 5.0.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + 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/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yuku-ast@0.9.3: + resolution: {integrity: sha512-Kt0PXlPCXdKF1fWFTl7N3DaxsVk6DHF8VAXHJMkwPtJnWYgbx20pYgGSweuC/86mIM7k1NUITQ4JffgOLUWTCw==} + + yuku-codegen@0.9.3: + resolution: {integrity: sha512-7oTWwetHiMSyrVPFb8089lBBqkU1gaeQZyumNBJ0t5hocMQRaWhnMeSXTz7J1xm/rcw9+ePsajORdZbub2C35w==} + + yuku-parser@0.9.3: + resolution: {integrity: sha512-96wPoHnwaXfkZv7UIOUkDb+s7ZH8lv8Qtg+MxdvHUV1LFa5jV9iKOaams1942YQt+krFQrWiD6lSg2ermv5kHA==} + + zod@4.5.4: + resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} + +snapshots: + + '@andrewbranch/untar.js@1.0.4': {} + + '@arethetypeswrong/cli@0.18.5': + dependencies: + '@arethetypeswrong/core': 0.18.5 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.3.0(marked@9.1.6) + semver: 7.8.5 + + '@arethetypeswrong/core@0.18.5': + dependencies: + '@andrewbranch/untar.js': 1.0.4 + '@loaderkit/resolve': 1.0.6 + cjs-module-lexer: 1.4.3 + fflate: 0.8.3 + lru-cache: 11.5.2 + semver: 7.8.5 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + + '@braidai/lang@1.1.2': {} + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@colors/colors@1.5.0': + optional: true + + '@cto.af/wtf8@0.0.5': {} + + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0))': + dependencies: + eslint: 10.10.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.10.0(jiti@2.7.0))': + optionalDependencies: + eslint: 10.10.0(jiti@2.7.0) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exadev/eslint-config@2.10.6(eslint@10.10.0(jiti@2.7.0))(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(typescript@6.0.3)': + dependencies: + '@eslint/js': 10.0.1(eslint@10.10.0(jiti@2.7.0)) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + typescript-eslint: 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@loaderkit/resolve@1.0.6': + dependencies: + '@braidai/lang': 1.1.2 + + '@oxc-project/types@0.148.0': {} + + '@pkgr/core@0.3.6': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/is@4.6.0': {} + + '@turbo/darwin-64@2.10.12': + optional: true + + '@turbo/darwin-arm64@2.10.12': + optional: true + + '@turbo/linux-64@2.10.12': + optional: true + + '@turbo/linux-arm64@2.10.12': + optional: true + + '@turbo/windows-64@2.10.12': + optional: true + + '@turbo/windows-arm64@2.10.12': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.4.1': + dependencies: + undici-types: 8.3.0 + + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/type-utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.69.0 + eslint: 10.10.0(jiti@2.7.0) + ignore: 7.0.8 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.69.0 + debug: 4.4.3 + eslint: 10.10.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.69.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3) + '@typescript-eslint/types': 8.69.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.69.0': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + + '@typescript-eslint/tsconfig-utils@8.69.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.10.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.69.0': {} + + '@typescript-eslint/typescript-estree@8.69.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.69.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.69.0': + dependencies: + '@typescript-eslint/types': 8.69.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/mocker@5.0.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0 + estree-walker: 3.0.3 + magic-string: 1.2.3 + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@vitest/spy@5.0.0': {} + + '@yuku-codegen/binding-android-arm64@0.9.3': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.9.3': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.9.3': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.9.3': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.9.3': + optional: true + + '@yuku-codegen/binding-win32-x64@0.9.3': + optional: true + + '@yuku-parser/binding-android-arm64@0.9.3': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.9.3': + optional: true + + '@yuku-parser/binding-darwin-x64@0.9.3': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.9.3': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.9.3': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.9.3': + optional: true + + '@yuku-parser/binding-win32-arm64@0.9.3': + optional: true + + '@yuku-parser/binding-win32-x64@0.9.3': + optional: true + + '@yuku-toolchain/types@0.9.3': {} + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + cac@7.0.0: {} + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + camelcase@9.0.0: {} + + cbor2@2.3.0: + dependencies: + '@cto.af/wtf8': 0.0.5 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + char-regex@1.0.2: {} + + cjs-module-lexer@1.4.3: {} + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.2 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + defu@6.1.7: {} + + detect-libc@2.1.2: {} + + dts-resolver@3.0.0: {} + + emoji-regex@8.0.0: {} + + emojilib@2.4.0: {} + + empathic@2.0.1: {} + + environment@1.1.0: {} + + es-module-lexer@2.3.2: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)): + dependencies: + eslint: 10.10.0(jiti@2.7.0) + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)))(eslint@10.10.0(jiti@2.7.0))(prettier@3.9.6): + dependencies: + eslint: 10.10.0(jiti@2.7.0) + prettier: 3.9.6 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.10.0(jiti@2.7.0)) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.10.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fflate@0.8.3: {} + + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.12.0: {} + + has-flag@4.0.0: {} + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + highlight.js@10.7.3: {} + + hookable@6.1.1: {} + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + ignore@5.3.2: {} + + ignore@7.0.8: {} + + import-without-cache@0.4.0: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@11.5.2: {} + + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + marked-terminal@7.3.0(marked@9.1.6): + dependencies: + ansi-escapes: 7.3.0 + ansi-regex: 6.3.0 + chalk: 5.6.2 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 9.1.6 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + + marked@9.1.6: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.18: {} + + natural-compare@1.4.0: {} + + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + object-assign@4.1.1: {} + + obug@2.1.4: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.6: {} + + punycode@2.3.1: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + quansync@1.0.0: {} + + require-directory@2.1.1: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.28.5(rolldown@1.2.7)(typescript@6.0.3): + dependencies: + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.6 + obug: 2.1.4 + rolldown: 1.2.7 + yuku-ast: 0.9.3 + yuku-codegen: 0.9.3 + yuku-parser: 0.9.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@6.1.4: {} + + tinyexec@1.3.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tsdown@0.23.0(@arethetypeswrong/core@0.18.5)(typescript@6.0.3): + dependencies: + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.4 + picomatch: 4.0.7 + rolldown: 1.2.7 + rolldown-plugin-dts: 0.28.5(rolldown@1.2.7)(typescript@6.0.3) + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + verkit: 0.4.0 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + typescript: 6.0.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - oxc-resolver + - vue-tsc + + turbo@2.10.12: + optionalDependencies: + '@turbo/darwin-64': 2.10.12 + '@turbo/darwin-arm64': 2.10.12 + '@turbo/linux-64': 2.10.12 + '@turbo/linux-arm64': 2.10.12 + '@turbo/windows-64': 2.10.12 + '@turbo/windows-arm64': 2.10.12 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@5.6.1-rc: {} + + typescript@6.0.3: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + undici-types@8.3.0: {} + + unicode-emoji-modifier-base@1.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + validate-npm-package-name@5.0.1: {} + + verkit@0.4.0: {} + + vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.4.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.2.3 + obug: 2.1.4 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.4.1 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yocto-queue@0.1.0: {} + + yuku-ast@0.9.3: + dependencies: + '@yuku-toolchain/types': 0.9.3 + + yuku-codegen@0.9.3: + dependencies: + '@yuku-toolchain/types': 0.9.3 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.9.3 + '@yuku-codegen/binding-darwin-arm64': 0.9.3 + '@yuku-codegen/binding-darwin-x64': 0.9.3 + '@yuku-codegen/binding-freebsd-x64': 0.9.3 + '@yuku-codegen/binding-linux-arm-gnu': 0.9.3 + '@yuku-codegen/binding-linux-arm-musl': 0.9.3 + '@yuku-codegen/binding-linux-arm64-gnu': 0.9.3 + '@yuku-codegen/binding-linux-arm64-musl': 0.9.3 + '@yuku-codegen/binding-linux-x64-gnu': 0.9.3 + '@yuku-codegen/binding-linux-x64-musl': 0.9.3 + '@yuku-codegen/binding-win32-arm64': 0.9.3 + '@yuku-codegen/binding-win32-x64': 0.9.3 + + yuku-parser@0.9.3: + dependencies: + '@yuku-toolchain/types': 0.9.3 + yuku-ast: 0.9.3 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.9.3 + '@yuku-parser/binding-darwin-arm64': 0.9.3 + '@yuku-parser/binding-darwin-x64': 0.9.3 + '@yuku-parser/binding-freebsd-x64': 0.9.3 + '@yuku-parser/binding-linux-arm-gnu': 0.9.3 + '@yuku-parser/binding-linux-arm-musl': 0.9.3 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.3 + '@yuku-parser/binding-linux-arm64-musl': 0.9.3 + '@yuku-parser/binding-linux-x64-gnu': 0.9.3 + '@yuku-parser/binding-linux-x64-musl': 0.9.3 + '@yuku-parser/binding-win32-arm64': 0.9.3 + '@yuku-parser/binding-win32-x64': 0.9.3 + + zod@4.5.4: {} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..f37ad88 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,15 @@ +// Usage: node src/cli.ts +import { writeFileSync } from "node:fs"; +import { parse } from "../vendor/cddl/dist/index.js"; +import { emitModule } from "./emitter.ts"; + +const [, , input, output] = process.argv; +if (input === undefined || output === undefined) { + console.error("usage: cddl.js "); + process.exit(1); +} + +const parsed = parse(input); +const source = emitModule(parsed); +writeFileSync(output, source); +console.log(`wrote ${output}`); diff --git a/src/emitter.ts b/src/emitter.ts new file mode 100644 index 0000000..275eb59 --- /dev/null +++ b/src/emitter.ts @@ -0,0 +1,507 @@ +// Walks a parsed CDDL AST (from vendor/cddl) and emits TypeScript source defining a Zod schema per named rule, plus its inferred type. Scoped deliberately to the CDDL subset wire-mesh's spec/*.cddl actually uses -- documented in README.md -- not full RFC 8610. Anything outside that subset throws a clear error naming the unsupported construct and the rule it appeared in, rather than silently emitting something wrong. + +import camelCase from "camelcase"; + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +// `Array.isArray`'s lib.es5.d.ts signature is `(arg: any) => arg is any[]`, so guarding an `unknown` with it narrows to `any[]`, not `unknown[]` -- indexing the result then silently produces `any`. This wrapper narrows to `unknown[]` instead, so every element access downstream still requires its own explicit check. +function isUnknownArray(value: unknown): value is unknown[] { + return Array.isArray(value); +} + +function isRawRule(value: unknown): value is { Type: string; Name: string } { + return ( + isPlainObject(value) && + typeof value.Type === "string" && + typeof value.Name === "string" + ); +} + +interface OccurrenceShape { + n: number; + m: number; +} + +function isOccurrence(value: unknown): value is OccurrenceShape { + return ( + isPlainObject(value) && + typeof value.n === "number" && + typeof value.m === "number" + ); +} + +interface LiteralValueNode { + Type: string; + Value: unknown; +} + +function isLiteralValueNode(value: unknown): value is LiteralValueNode { + return ( + isPlainObject(value) && typeof value.Type === "string" && "Value" in value + ); +} + +/** A named rule, after socket (`$name /= X`) choice-additions for the same name have been merged into one combined union. A socket's own entries can mix AST kinds -- e.g. `$manage-command-params = {* tstr => any}` (a group) extended by `$manage-command-params /= pty-spawn / ...` (a variable union of group references) -- so `raw` carries each entry's own kind rather than one shared kind for the whole rule; emitEntryExpr dispatches per entry. `raw` is a non-empty tuple type -- mergeRules never produces a MergedRule without at least one entry -- so callers can index `raw[0]` without an undefined check. */ +interface MergedRule { + name: string; + isSocket: boolean; + raw: [Record, ...Record[]]; +} + +export function mergeRules(parsed: unknown): MergedRule[] { + if (!isUnknownArray(parsed)) { + throw new Error("expected parse() to return an array of top-level rules"); + } + + const byName = new Map(); + for (const entry of parsed) { + if (!isRawRule(entry)) { + throw new Error( + `unrecognised top-level parse() entry: ${JSON.stringify(entry)}`, + ); + } + if ( + entry.Type !== "group" && + entry.Type !== "array" && + entry.Type !== "variable" + ) { + throw new Error( + `unsupported top-level rule kind "${entry.Type}" for rule "${entry.Name}"`, + ); + } + + const existing = byName.get(entry.Name); + if (existing) { + if (!existing.isSocket) { + throw new Error( + `rule "${entry.Name}" is defined more than once, outside cddl.js's supported subset (only a socket name, "$name /= X", may have multiple definitions)`, + ); + } + existing.raw.push(entry); + continue; + } + + byName.set(entry.Name, { + name: entry.Name, + isSocket: entry.Name.startsWith("$"), + raw: [entry], + }); + } + + return [...byName.values()]; +} + +function schemaVarName(ruleName: string): string { + const clean = ruleName.startsWith("$") ? ruleName.slice(1) : ruleName; + return `${camelCase(clean)}Schema`; +} + +function typeName(ruleName: string): string { + const clean = ruleName.startsWith("$") ? ruleName.slice(1) : ruleName; + return camelCase(clean, { pascalCase: true }); +} + +function requireStringField( + obj: Record, + key: string, + ruleName: string, +): string { + const value = obj[key]; + if (typeof value !== "string") { + throw new Error( + `rule "${ruleName}" is missing the expected "${key}" string field`, + ); + } + return value; +} + +function requireObjectField( + obj: Record, + key: string, + ruleName: string, +): Record { + const value = obj[key]; + if (!isPlainObject(value)) { + throw new Error( + `rule "${ruleName}" is missing the expected "${key}" field`, + ); + } + return value; +} + +function emitOccurrence( + occurrence: unknown, + inner: string, + ruleName: string, +): { expr: string; optional: boolean } { + if (!isOccurrence(occurrence)) { + throw new Error( + `rule "${ruleName}" has a member with an unrecognised Occurrence`, + ); + } + const { n, m } = occurrence; + if (n === 1 && m === 1) { + return { expr: inner, optional: false }; + } + if (n === 0 && m === Infinity) { + return { expr: inner, optional: true }; + } + if (n === 0 && m === 1) { + return { expr: `${inner}.optional()`, optional: false }; + } + throw new Error( + `rule "${ruleName}" uses an occurrence indicator (n=${String(n)}, m=${String(m)}) outside cddl.js's supported subset (?, required, or unbounded *)`, + ); +} + +function emitOperatorValue( + operator: Record, + ruleName: string, +): string { + const opValue = operator.Value; + if (isLiteralValueNode(opValue) && opValue.Type === "literal") { + return JSON.stringify(opValue.Value); + } + throw new Error( + `rule "${ruleName}" uses an unsupported operator value shape`, + ); +} + +function emitWithOperator( + node: Record, + ruleName: string, +): string { + const baseType = node.Type; + const operator = requireObjectField(node, "Operator", ruleName); + const operatorType = operator.Type; + + let baseExpr: string; + if (typeof baseType === "string") { + baseExpr = emitNativeType(baseType, ruleName); + } else if ( + isPlainObject(baseType) && + baseType.Type === "group" && + typeof baseType.Value === "string" + ) { + baseExpr = `z.lazy(() => ${schemaVarName(baseType.Value)})`; + } else { + throw new Error( + `rule "${ruleName}" has an operator attached to an unsupported base type`, + ); + } + + if (operatorType === "size") { + const opValue = operator.Value; + if ( + !isLiteralValueNode(opValue) || + opValue.Type !== "literal" || + typeof opValue.Value !== "number" + ) { + throw new Error( + `rule "${ruleName}" uses .size with a non-literal-integer value, outside cddl.js's supported subset`, + ); + } + const size = opValue.Value; + return `${baseExpr}.refine((v) => v.length === ${String(size)}, { message: "expected exactly ${String(size)} bytes" })`; + } + + if (operatorType === "regexp") { + const pattern = emitOperatorValue(operator, ruleName); + return `${baseExpr}.regex(new RegExp(${pattern}))`; + } + + if (operatorType === "cbor" || operatorType === "cborseq") { + const opValue = operator.Value; + if ( + !isPlainObject(opValue) || + opValue.Type !== "group" || + typeof opValue.Value !== "string" + ) { + throw new Error( + `rule "${ruleName}" uses .cbor/.cborseq with a value that isn't a plain rule reference, outside cddl.js's supported subset`, + ); + } + const innerVar = schemaVarName(opValue.Value); + return `cborDecodesAs(z.lazy(() => ${innerVar}))`; + } + + throw new Error( + `rule "${ruleName}" uses unsupported operator ".${String(operatorType)}"`, + ); +} + +function emitNativeType(name: string, ruleName: string): string { + switch (name) { + case "bstr": + return "z.instanceof(Uint8Array)"; + case "tstr": + return "z.string()"; + case "int": + return "z.number().int()"; + case "uint": + return "z.number().int().nonnegative()"; + case "nint": + return "z.number().int().negative()"; + case "bool": + return "z.boolean()"; + case "any": + return "z.unknown()"; + case "nil": + case "null": + return "z.null()"; + default: + throw new Error( + `rule "${ruleName}" uses native type "${name}", outside cddl.js's supported subset`, + ); + } +} + +/** Resolves a rule name to the single literal value it's an alias for (e.g. `cose-header-alg = 1` resolves "cose-header-alg" to `1`) -- built once per emitModule call and threaded through every emit* function that walks into a map's properties, since an arrow-syntax map entry keyed by such a name (`? cose-header-alg => int`) must use the literal's own wire value as the object key, not the rule's name (wire-mesh's codec decodes an integer-keyed CBOR map to a plain object keyed by String(n), confirmed against conformance/codec.ts's fromWire). */ +type LiteralKeyMap = ReadonlyMap; + +function buildLiteralKeyMap(rules: readonly MergedRule[]): LiteralKeyMap { + const map = new Map(); + for (const rule of rules) { + if (rule.raw.length !== 1) continue; + const entry = rule.raw[0]; + if (entry.Type !== "variable") continue; + const propertyType = entry.PropertyType; + if (!isUnknownArray(propertyType) || propertyType.length !== 1) continue; + const only = propertyType[0]; + if ( + isLiteralValueNode(only) && + only.Type === "literal" && + (typeof only.Value === "string" || typeof only.Value === "number") + ) { + map.set(rule.name, only.Value); + } + } + return map; +} + +function emitSingleType( + node: unknown, + ruleName: string, + literalKeys: LiteralKeyMap, +): string { + if (typeof node === "string") { + return emitNativeType(node, ruleName); + } + if (!isPlainObject(node)) { + throw new Error( + `rule "${ruleName}" has a property type that isn't a recognised shape: ${JSON.stringify(node)}`, + ); + } + if ("Operator" in node) { + return emitWithOperator(node, ruleName); + } + if (node.Type === "literal") { + return `z.literal(${JSON.stringify(node.Value)})`; + } + if (node.Type === "group" && typeof node.Value === "string") { + return `z.lazy(() => ${schemaVarName(node.Value)})`; + } + if (node.Type === "group" && "Properties" in node) { + // An inline anonymous map used as a property's own value type, e.g. `env: {* tstr => tstr}` -- distinct from the rule-reference shape above, which carries a `Value` name instead of its own `Properties`. + return emitGroupExpr(node, ruleName, literalKeys); + } + if (node.Type === "array") { + return emitArrayExpr(node, ruleName, literalKeys); + } + throw new Error( + `rule "${ruleName}" has a property type shape outside cddl.js's supported subset: ${JSON.stringify(node)}`, + ); +} + +/** A property type is normally an array of branch nodes (one per `/`-separated alternative), but the vendored parser emits a bare node directly -- not array-wrapped -- for an array's own anonymous, single-member value type (confirmed against wire-mesh's real spec: `entries: [* bstr]` produces a bare `"bstr"` where `peers: [* peer-advert]` produces a wrapped `[{Type:"group",...}]`). Both shapes are handled uniformly here. */ +function emitPropertyType( + types: unknown, + ruleName: string, + literalKeys: LiteralKeyMap, +): string { + if (!isUnknownArray(types)) { + return emitSingleType(types, ruleName, literalKeys); + } + if (types.length === 0) { + throw new Error(`rule "${ruleName}" has an empty property type`); + } + if (types.length === 1) { + return emitSingleType(types[0], ruleName, literalKeys); + } + return `z.union([${types.map((t) => emitSingleType(t, ruleName, literalKeys)).join(", ")}])`; +} + +function emitGroupProperties( + properties: unknown, + ruleName: string, + literalKeys: LiteralKeyMap, +): { fields: string[]; catchall: string | undefined } { + if (!isUnknownArray(properties)) { + throw new Error(`rule "${ruleName}" has malformed Properties`); + } + + const fields: string[] = []; + let catchall: string | undefined; + + for (const rawProp of properties) { + if (!isPlainObject(rawProp)) { + throw new Error(`rule "${ruleName}" has a malformed property entry`); + } + const occurrence = rawProp.Occurrence; + const keyName = requireStringField(rawProp, "Name", ruleName); + + if (rawProp.HasCut === false) { + // Arrow syntax (`keytype => valuetype`): either a specific literal-valued key (`? cose-header-alg => int`) or the generic open-map-tail (`* tstr => any`). + const literalKey = literalKeys.get(keyName); + if (literalKey !== undefined) { + const inner = emitPropertyType(rawProp.Type, ruleName, literalKeys); + const { expr, optional } = emitOccurrence(occurrence, inner, ruleName); + fields.push( + ` ${JSON.stringify(String(literalKey))}: ${optional ? `${expr}.optional()` : expr},`, + ); + continue; + } + if ( + isOccurrence(occurrence) && + occurrence.n === 0 && + occurrence.m === Infinity + ) { + if (catchall !== undefined) { + throw new Error( + `rule "${ruleName}" has more than one open-map-tail (* key => value) entry, outside cddl.js's supported subset (one per map)`, + ); + } + catchall = emitPropertyType(rawProp.Type, ruleName, literalKeys); + continue; + } + throw new Error( + `rule "${ruleName}" has an arrow-syntax map entry ("${keyName} => ...") that is neither a known literal-valued key nor the open-tail pattern, outside cddl.js's supported subset`, + ); + } + + const inner = emitPropertyType(rawProp.Type, ruleName, literalKeys); + const { expr, optional } = emitOccurrence(occurrence, inner, ruleName); + fields.push( + ` ${JSON.stringify(keyName)}: ${optional ? `${expr}.optional()` : expr},`, + ); + } + + return { fields, catchall }; +} + +function emitGroupExpr( + entry: Record, + ruleName: string, + literalKeys: LiteralKeyMap, +): string { + const { fields, catchall } = emitGroupProperties( + entry.Properties, + ruleName, + literalKeys, + ); + const object = `z.object({\n${fields.join("\n")}\n})`; + return catchall !== undefined ? `${object}.catchall(${catchall})` : object; +} + +function emitArrayExpr( + entry: Record, + ruleName: string, + literalKeys: LiteralKeyMap, +): string { + const values = entry.Values; + if (!isUnknownArray(values) || values.length === 0) { + throw new Error(`rule "${ruleName}" has malformed or empty array Values`); + } + + // A single, unnamed, unbounded-occurrence member is a homogeneous repeated array: `[* T]`. + if (values.length === 1) { + const only = values[0]; + if (isPlainObject(only)) { + const occurrence = only.Occurrence; + if ( + only.Name === "" && + isOccurrence(occurrence) && + occurrence.n === 0 && + occurrence.m === Infinity + ) { + const inner = emitPropertyType(only.Type, ruleName, literalKeys); + return `z.array(${inner})`; + } + } + } + + // Otherwise: a fixed, heterogeneous tuple, e.g. cose-sign1's [protected, unprotected, payload, signature]. + const elements = values.map((value) => { + if (!isPlainObject(value)) { + throw new Error(`rule "${ruleName}" has a malformed array member`); + } + const occurrence = value.Occurrence; + if (!isOccurrence(occurrence) || occurrence.n !== 1 || occurrence.m !== 1) { + throw new Error( + `rule "${ruleName}" mixes occurrence indicators within a fixed array, outside cddl.js's supported subset`, + ); + } + return emitPropertyType(value.Type, ruleName, literalKeys); + }); + return `z.tuple([${elements.join(", ")}])`; +} + +/** Emits one raw AST entry's own bare Zod expression (no `z.lazy`/`export const` wrapper), dispatching on that entry's own `Type` -- not the rule's, since a socket's entries can mix kinds (see MergedRule). */ +function emitEntryExpr( + entry: Record, + ruleName: string, + literalKeys: LiteralKeyMap, +): string { + const kind = entry.Type; + if (kind === "group") { + return emitGroupExpr(entry, ruleName, literalKeys); + } + if (kind === "array") { + return emitArrayExpr(entry, ruleName, literalKeys); + } + if (kind === "variable") { + return emitPropertyType(entry.PropertyType, ruleName, literalKeys); + } + throw new Error( + `rule "${ruleName}" has an unsupported top-level rule kind "${String(kind)}"`, + ); +} + +/** A rule with exactly one entry emits that entry's own expression directly; a socket with several entries (its base definition plus every `/=` choice-addition) unions every entry's own expression together. */ +function emitRule(rule: MergedRule, literalKeys: LiteralKeyMap): string { + const exprs = rule.raw.map((entry) => + emitEntryExpr(entry, rule.name, literalKeys), + ); + const expr = + rule.raw.length === 1 ? exprs.join("") : `z.union([${exprs.join(", ")}])`; + return `export const ${schemaVarName(rule.name)}: z.ZodType = z.lazy(() => ${expr});`; +} + +/** Compiles a parsed CDDL AST into a single TypeScript module source: one Zod schema plus one inferred type export per named rule. */ +export function emitModule(parsed: unknown): string { + const rules = mergeRules(parsed); + const literalKeys = buildLiteralKeyMap(rules); + + const schemaLines: string[] = []; + const typeLines: string[] = []; + const needsCbor = JSON.stringify(parsed).includes('"cbor"'); + + for (const rule of rules) { + schemaLines.push(emitRule(rule, literalKeys)); + typeLines.push( + `export type ${typeName(rule.name)} = z.infer;`, + ); + } + + const header = [ + "// Generated by cddl.js. Do not edit by hand -- regenerate from the source .cddl instead.", + "", + 'import { z } from "zod";', + needsCbor ? 'import { cborDecodesAs } from "./runtime.js";' : undefined, + "", + ].filter((line): line is string => line !== undefined); + + return [...header, ...schemaLines, "", ...typeLines, ""].join("\n"); +} diff --git a/src/runtime.ts b/src/runtime.ts new file mode 100644 index 0000000..3bbed69 --- /dev/null +++ b/src/runtime.ts @@ -0,0 +1,27 @@ +// Small runtime support a generated module imports when its CDDL used the `.cbor`/`.cborseq` control operator: a byte string whose own decoded contents must validate against another schema (how COSE_Sign1's protected/payload fields nest a further CBOR-encoded value inside a bstr). + +import { z } from "zod"; +import { decode, cdeDecodeOptions } from "cbor2"; + +// Validates the embedded CBOR without transforming the field's own value: the original bytes pass through unchanged on success. A transform that returned the decoded value instead would make the outer schema's re-encoding produce a different byte string than the one that was decoded -- correctness for cddl.js's own round-trip guarantee (decode a conformance vector, validate it, re-encode it, get the identical wire bytes back) takes priority over the ergonomic of an already-decoded nested value; a caller that wants the inner value decodes it separately. The passed-in schema's own inferred type plays no part in the result (only its safeParse behaviour matters), so it takes no type parameter. +export function cborDecodesAs(schema: z.ZodType): z.ZodType { + return z.instanceof(Uint8Array).superRefine((bytes, ctx) => { + let decoded: unknown; + try { + decoded = decode(bytes, cdeDecodeOptions); + } catch (error) { + ctx.addIssue({ + code: "custom", + message: `bytes do not decode as CBOR: ${String(error)}`, + }); + return; + } + const result = schema.safeParse(decoded); + if (!result.success) { + ctx.addIssue({ + code: "custom", + message: `decoded CBOR does not match the expected type: ${result.error.message}`, + }); + } + }); +} diff --git a/test/fixtures/README.md b/test/fixtures/README.md new file mode 100644 index 0000000..7831b58 --- /dev/null +++ b/test/fixtures/README.md @@ -0,0 +1,8 @@ +Test fixtures copied verbatim from [ExaDev/wire-mesh](https://github.com/ExaDev/wire-mesh): `spec/protocol.cddl` and `conformance/{frames,handshake,tokens}.v1.json`. wire-mesh's own spec is the real-world CDDL cddl.js is scoped to generate against, and its conformance vectors are the ground truth `test/round-trip.test.ts` checks every generated schema against. + +These are a snapshot, not a live reference -- refresh by copying the same files again from a wire-mesh checkout whenever its spec or vectors change: + +```sh +cp /spec/protocol.cddl test/fixtures/protocol.cddl +cp /conformance/{frames,handshake,tokens}.v1.json test/fixtures/ +``` diff --git a/test/fixtures/frames.v1.json b/test/fixtures/frames.v1.json new file mode 100644 index 0000000..a45fd60 --- /dev/null +++ b/test/fixtures/frames.v1.json @@ -0,0 +1,361 @@ +{ + "protocol_version": 1, + "description": "Frame conformance vectors for protocol version 1, covering every $frame-variant in spec/frame.cddl except handshake-frame (see handshake.v1.json). manage-response-frame gets two vectors, one per branch of its manage-ok / manage-error outcome union.", + "vectors": [ + { + "name": "ping_v1", + "message": { + "type": "ping" + }, + "wire_hex": "a164747970656470696e67" + }, + { + "name": "close_v1_with_reason", + "message": { + "type": "close", + "reason": "shutting down" + }, + "wire_hex": "a2647479706565636c6f736566726561736f6e6d7368757474696e6720646f776e" + }, + { + "name": "gossip_v1_two_peers", + "message": { + "type": "gossip", + "peers": [ + { + "device": { + "hex": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "addresses": [ + "203.0.113.5:4433" + ], + "snapshot-seconds": 1861833600 + }, + { + "device": { + "hex": "2222222222222222222222222222222222222222222222222222222222222222" + }, + "addresses": [ + "203.0.113.9:4433", + "198.51.100.2:4433" + ], + "snapshot-seconds": 1861833601 + } + ] + }, + "wire_hex": "a2647479706566676f7373697065706565727382a366646576696365582011111111111111111111111111111111111111111111111111111111111111116961646472657373657381703230332e302e3131332e353a3434333370736e617073686f742d7365636f6e64731a6ef95380a366646576696365582022222222222222222222222222222222222222222222222222222222222222226961646472657373657382703230332e302e3131332e393a34343333713139382e35312e3130302e323a3434333370736e617073686f742d7365636f6e64731a6ef95381" + }, + { + "name": "candidates_v1_host_and_relayed", + "message": { + "type": "candidates", + "candidates": [ + { + "address": "203.0.113.5:4433", + "kind": "host", + "priority": 100 + }, + { + "address": "198.51.100.2:7000", + "kind": "relayed", + "priority": 10 + } + ] + }, + "wire_hex": "a264747970656a63616e646964617465736a63616e6469646174657382a3646b696e6464686f73746761646472657373703230332e302e3131332e353a34343333687072696f726974791864a3646b696e646772656c617965646761646472657373713139382e35312e3130302e323a37303030687072696f726974790a" + }, + { + "name": "sync_punch_v1", + "message": { + "type": "sync-punch", + "nonce": 42, + "deadline-unix-ms": 1861833605000 + }, + "wire_hex": "a364747970656a73796e632d70756e6368656e6f6e6365182a70646561646c696e652d756e69782d6d731b000001b17dee3f88" + }, + { + "name": "observed_address_v1", + "message": { + "type": "observed-address", + "address": "203.0.113.5:51820" + }, + "wire_hex": "a26474797065706f627365727665642d616464726573736761646472657373713230332e302e3131332e353a3531383230" + }, + { + "name": "relay_offer_v1", + "message": { + "type": "relay-offer", + "addresses": [ + "198.51.100.2:7000" + ] + }, + "wire_hex": "a264747970656b72656c61792d6f666665726961646472657373657381713139382e35312e3130302e323a37303030" + }, + { + "name": "relay_connect_v1", + "message": { + "type": "relay-connect", + "target-device": { + "hex": "3333333333333333333333333333333333333333333333333333333333333333" + } + }, + "wire_hex": "a264747970656d72656c61792d636f6e6e6563746d7461726765742d64657669636558203333333333333333333333333333333333333333333333333333333333333333" + }, + { + "name": "relay_data_v1", + "message": { + "type": "relay-data", + "payload": { + "hex": "dededededededededededededededededededededededede" + } + }, + "wire_hex": "a264747970656a72656c61792d64617461677061796c6f61645818dededededededededededededededededededededededede" + }, + { + "name": "relay_inbound_v1", + "message": { + "type": "relay-inbound", + "source-device": { + "hex": "2222222222222222222222222222222222222222222222222222222222222222" + } + }, + "wire_hex": "a264747970656d72656c61792d696e626f756e646d736f757263652d64657669636558202222222222222222222222222222222222222222222222222222222222222222" + }, + { + "name": "manage_request_v1_pty_spawn", + "message": { + "type": "manage-request", + "request-id": 1, + "command": { + "verb": "exec:pty", + "params": { + "verb": "pty.spawn", + "shell": "/bin/sh", + "argv": [], + "cwd": "/work", + "env": {}, + "cols": 80, + "rows": 24 + } + }, + "scope": { + "kind": "folder", + "path": "/work" + } + }, + "wire_hex": "a464747970656e6d616e6167652d726571756573746573636f7065a2646b696e6466666f6c6465726470617468652f776f726b67636f6d6d616e64a2647665726268657865633a70747966706172616d73a763637764652f776f726b63656e76a064617267768064636f6c73185064726f777318186476657262697074792e737061776e657368656c6c672f62696e2f73686a726571756573742d696401" + }, + { + "name": "manage_response_v1_ok", + "message": { + "type": "manage-response", + "request-id": 1, + "outcome": { + "result": "ok" + } + }, + "wire_hex": "a364747970656f6d616e6167652d726573706f6e7365676f7574636f6d65a166726573756c74626f6b6a726571756573742d696401" + }, + { + "name": "manage_response_v1_error", + "message": { + "type": "manage-response", + "request-id": 2, + "outcome": { + "result": "error", + "code": "scope-denied", + "message": "token does not authorise this path" + } + }, + "wire_hex": "a364747970656f6d616e6167652d726573706f6e7365676f7574636f6d65a364636f64656c73636f70652d64656e69656466726573756c74656572726f72676d6573736167657822746f6b656e20646f6573206e6f7420617574686f72697365207468697320706174686a726571756573742d696402" + }, + { + "name": "revocation_announce_v1_two_entries", + "message": { + "type": "revocation-announce", + "entries": [ + { + "token-id": { + "hex": "01010101010101010101010101010101" + }, + "revoked-at": 1861833700000 + }, + { + "token-id": { + "hex": "02020202020202020202020202020202" + }, + "revoked-at": 1861833701000 + } + ] + }, + "wire_hex": "a26474797065737265766f636174696f6e2d616e6e6f756e636567656e747269657382a268746f6b656e2d696450010101010101010101010101010101016a7265766f6b65642d61741b000001b17defb2a0a268746f6b656e2d696450020202020202020202020202020202026a7265766f6b65642d61741b000001b17defb688" + }, + { + "name": "stream_data_v1_stdout_chunk", + "message": { + "type": "stream-data", + "session": 7, + "seq": 3, + "channel": "stdout", + "bytes": { + "hex": "68656c6c6f0a" + } + }, + "wire_hex": "a5637365710364747970656b73747265616d2d646174616562797465734668656c6c6f0a676368616e6e656c667374646f75746773657373696f6e07" + }, + { + "name": "stream_ack_v1", + "message": { + "type": "stream-ack", + "session": 7, + "ack-seq": 3, + "window": 65536 + }, + "wire_hex": "a464747970656a73747265616d2d61636b6677696e646f771a000100006761636b2d736571036773657373696f6e07" + }, + { + "name": "stream_end_v1_exit_code", + "message": { + "type": "stream-end", + "session": 7, + "exit-code": 0 + }, + "wire_hex": "a364747970656a73747265616d2d656e646773657373696f6e0769657869742d636f646500" + }, + { + "name": "data_have_v1", + "message": { + "type": "data-have", + "peer": { + "hex": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "head-seq": 128 + }, + "wire_hex": "a3647065657258201111111111111111111111111111111111111111111111111111111111111111647479706569646174612d6861766568686561642d7365711880" + }, + { + "name": "data_request_v1", + "message": { + "type": "data-request", + "peer": { + "hex": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "from-seq": 100 + }, + "wire_hex": "a364706565725820111111111111111111111111111111111111111111111111111111111111111164747970656c646174612d726571756573746866726f6d2d7365711864" + }, + { + "name": "data_entries_v1_two_entries", + "message": { + "type": "data-entries", + "peer": { + "hex": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "from-seq": 100, + "entries": [ + { + "hex": "aabbcc" + }, + { + "hex": "ddeeff00" + } + ] + }, + "wire_hex": "a464706565725820111111111111111111111111111111111111111111111111111111111111111164747970656c646174612d656e747269657367656e74726965738243aabbcc44ddeeff006866726f6d2d7365711864" + }, + { + "name": "federation_link_request_v1", + "message": { + "type": "federation-link-request", + "local-mesh": "exadev-internal", + "local-name": "exadev", + "offered-shares": [ + { + "domain": "core/data", + "resource": { + "kind": "room", + "path": "general" + }, + "direction": "outbound" + } + ] + }, + "wire_hex": "a464747970657766656465726174696f6e2d6c696e6b2d726571756573746a6c6f63616c2d6d6573686f6578616465762d696e7465726e616c6a6c6f63616c2d6e616d65666578616465766e6f6666657265642d73686172657381a366646f6d61696e69636f72652f64617461687265736f75726365a2646b696e6464726f6f6d64706174686767656e6572616c69646972656374696f6e686f7574626f756e64" + }, + { + "name": "federation_link_accept_v1", + "message": { + "type": "federation-link-accept", + "remote-mesh": "example-partner", + "remote-name": "partner", + "accepted-shares": [ + { + "domain": "core/data", + "resource": { + "kind": "room", + "path": "general" + }, + "direction": "outbound" + } + ] + }, + "wire_hex": "a464747970657666656465726174696f6e2d6c696e6b2d6163636570746b72656d6f74652d6d6573686f6578616d706c652d706172746e65726b72656d6f74652d6e616d6567706172746e65726f61636365707465642d73686172657381a366646f6d61696e69636f72652f64617461687265736f75726365a2646b696e6464726f6f6d64706174686767656e6572616c69646972656374696f6e686f7574626f756e64" + }, + { + "name": "federation_link_reject_v1", + "message": { + "type": "federation-link-reject", + "reason": "no shared domains accepted" + }, + "wire_hex": "a264747970657666656465726174696f6e2d6c696e6b2d72656a65637466726561736f6e781a6e6f2073686172656420646f6d61696e73206163636570746564" + }, + { + "name": "federation_share_v1", + "message": { + "type": "federation-share", + "share": { + "domain": "core/data", + "resource": { + "kind": "room", + "path": "incidents" + }, + "direction": "bidirectional" + } + }, + "wire_hex": "a264747970657066656465726174696f6e2d7368617265657368617265a366646f6d61696e69636f72652f64617461687265736f75726365a2646b696e6464726f6f6d647061746869696e636964656e747369646972656374696f6e6d6269646972656374696f6e616c" + }, + { + "name": "federation_unshare_v1", + "message": { + "type": "federation-unshare", + "share": { + "domain": "core/data", + "resource": { + "kind": "room", + "path": "incidents" + }, + "direction": "bidirectional" + } + }, + "wire_hex": "a264747970657266656465726174696f6e2d756e7368617265657368617265a366646f6d61696e69636f72652f64617461687265736f75726365a2646b696e6464726f6f6d647061746869696e636964656e747369646972656374696f6e6d6269646972656374696f6e616c" + }, + { + "name": "federation_envelope_v1_wrapping_a_ping", + "message": { + "type": "federation-envelope", + "origin-mesh": "example-partner", + "origin-device": { + "hex": "3333333333333333333333333333333333333333333333333333333333333333" + }, + "resource": { + "kind": "room", + "path": "general" + }, + "inner": { + "hex": "a164747970656470696e67" + } + }, + "wire_hex": "a564747970657366656465726174696f6e2d656e76656c6f706565696e6e65724ba164747970656470696e67687265736f75726365a2646b696e6464726f6f6d64706174686767656e6572616c6b6f726967696e2d6d6573686f6578616d706c652d706172746e65726d6f726967696e2d64657669636558203333333333333333333333333333333333333333333333333333333333333333" + } + ] +} diff --git a/test/fixtures/generated/runtime.ts b/test/fixtures/generated/runtime.ts new file mode 100644 index 0000000..7edc5ad --- /dev/null +++ b/test/fixtures/generated/runtime.ts @@ -0,0 +1,2 @@ +// A generated protocol module (written by test/round-trip.test.ts's beforeAll into this same directory) imports its cbor-decoding helper as a same-directory sibling, "./runtime.js" -- matching where cddl.js's own CLI writes output alongside src/runtime.ts. This re-export gives it that sibling here too, without duplicating runtime.ts's content. +export * from "../../../src/runtime.ts"; diff --git a/test/fixtures/handshake.v1.json b/test/fixtures/handshake.v1.json new file mode 100644 index 0000000..273045f --- /dev/null +++ b/test/fixtures/handshake.v1.json @@ -0,0 +1,33 @@ +{ + "protocol_version": 1, + "description": "Handshake conformance vectors for protocol version 1. A conformant codec must decode each wire_hex to the described message and re-encode that message to exactly wire_hex, using RFC 8949 4.2 core deterministic (DAG-CBOR-compatible) encoding.", + "vectors": [ + { + "name": "handshake_v1_management_exec_federation", + "message": { + "type": "handshake", + "version": 1, + "domains": [ + "core/management", + "core/exec", + "core/federation" + ] + }, + "wire_hex": "a364747970656968616e647368616b6567646f6d61696e73836f636f72652f6d616e6167656d656e7469636f72652f657865636f636f72652f66656465726174696f6e6776657273696f6e01" + }, + { + "name": "handshake_v1_with_forward_compatible_params", + "message": { + "type": "handshake", + "version": 1, + "domains": [ + "core/data" + ], + "params": { + "max-frame-size": 65536 + } + }, + "wire_hex": "a464747970656968616e647368616b6566706172616d73a16e6d61782d6672616d652d73697a651a0001000067646f6d61696e738169636f72652f646174616776657273696f6e01" + } + ] +} diff --git a/test/fixtures/protocol.cddl b/test/fixtures/protocol.cddl new file mode 100644 index 0000000..a6e0b1f --- /dev/null +++ b/test/fixtures/protocol.cddl @@ -0,0 +1,469 @@ +; core/data — the opaque, per-application data domain. Generalises Cascade's +; own oplog: each peer's own log is a single-writer, append-only sequence, +; so two peers' logs never block-conflict, and distributing them between +; peers is replication. The frame envelope (owning peer, sequence range, +; length of the entry set) is fixed here; the entry itself is opaque bytes to +; this schema and to any peer relaying it, so different applications — an +; agent-comms room message, a Cascade operation-log entry, anything else — +; can share a mesh without the transport layer needing to understand any of +; their content. A domain implementing core/data defines its own entry +; schema separately, in its own application-level spec, not here. + +; `peer` names whose log is being asked about; it is deliberately independent +; of which connection the frame travels over. A peer that has already +; replicated another device's log may answer data-have/data-request for that +; device on its own behalf — this is fan-out: a follower reaches an audience +; of any size by asking whichever already-replicating peer it can reach, not +; by holding a direct link to the original author. No wire change is needed +; for this; it follows from `peer` already being a plain reference rather +; than an implicit "you" per connection. A relay/mailbox device (see +; discovery.cddl's `mailbox` field) answering on behalf of a currently-offline +; peer is the same mechanism, not a separate one: it is simply a replicator +; that is reachable when the original author is not. +data-have-frame = { type: "data-have", peer: device-id, head-seq: uint } +data-request-frame = { type: "data-request", peer: device-id, from-seq: uint } ; exclusive lower bound + +; The i-th entry (zero-based) carries sequence (from-seq + 1 + i); a receiver +; rejects a data-entries-frame whose from-seq would leave a gap against what +; it already holds for that peer. +data-entries-frame = { type: "data-entries", peer: device-id, from-seq: uint, entries: [* bstr] } +; Handle discovery — resolving an arbitrary, human-readable handle you have +; never interacted with to a device-id, the way WebFinger resolves an +; actor URI. This is deliberately DNS-anchored rather than a protocol-native +; naming ledger: global uniqueness, spoofing resistance, and revocation for +; an internet-wide namespace are already-solved problems in DNS, and a +; from-scratch naming system would mean re-solving a harder version of the +; same problem DNS already handles. This is why WebFinger, ActivityPub, and +; Matrix all independently converged on the same DNS-anchored shape for +; exactly this category of problem. +; +; A handle is "@". It resolves out of band (this is never +; sent as a Frame over an existing mesh connection — resolution is how you +; get an address to connect to in the first place) via: +; +; GET https:///.well-known/wire-mesh/ +; +; returning the CBOR encoding of a handle-record. The record is +; self-certifying — signed by the same key it claims to belong to, the same +; pattern Cascade's own announce/DHT candidate records use — specifically so +; it stays safe to serve and cache through an untrusted intermediary (a CDN, +; a cache, an intermediate DNS resolver) rather than requiring the resolver +; to trust the HTTPS transport alone. A resolver MUST verify +; device-id == sha256(identity-key.public-key) and MUST check `expires` +; before trusting a resolved record. + +handle-claims = { + handle: tstr, ; "local-part@domain", matching what was requested + device-id: device-id, + identity-key: identity-key, ; verifier checks sha256(identity-key.public-key) == device-id + ? candidates: [* wire-candidate], ; optional connection hints, reusing transport.cddl's candidate shape + ? mailbox: device-id, ; optional: a relay/hub device holding core/data entries on this handle's behalf while it is offline — the same replication mechanism data-domain.cddl describes for fan-out, not a separate delivery system + issued: uint, ; Unix ms + expires: uint, ; Unix ms — records are refreshed periodically, mirroring a DNS TTL; a resolver must not trust an expired record +} + +; Reuses the same COSE_Sign1 envelope as capability-token, for the same +; reason: every implementation can verify it with an off-the-shelf COSE +; library rather than hand-rolling a second signing scheme alongside the +; token one. +handle-record = cose-sign1 ; payload = bstr .cbor handle-claims + +; Handle ownership without DNS control: a handle-claims record for the same +; handle MAY instead be published as an ordinary core/data entry, under the +; conventional scope { kind: "handle-registry" }, and reached by resolving +; via a federation link rather than DNS — a mesh's own root identity stands +; in for a domain, and a federated mesh can then resolve any handle the +; owning mesh has published, with no third party (no domain, no hosting +; provider) involved at all. This needs no new frame type: it is the same +; core/data replication (data-domain.cddl) and the same federation sharing +; (federation.cddl's share-descriptor) already defined, applied to handle +; records instead of application content — federation already has to resolve +; and reach a partner mesh's resources somehow, and a handle registry is +; simply one more resource a federation link can share. The tradeoff this +; accepts, deliberately: resolvability is bounded to the owning mesh plus +; whatever it is federated with, not the whole internet unconditionally — +; DNS remains the only path for genuinely unbounded, internet-wide discovery. +; +; A fully decentralised, third-party-free alternative with unbounded (not +; federation-scoped) reach — e.g. a DHT keyed by a hash of the handle string, +; mirroring Cascade's own Mainline DHT precedent — was considered and +; deliberately deferred, not built: nothing else in this protocol needs a +; DHT, so building one solely for this would be speculative generality +; against no concrete requirement, and it does not actually solve global +; uniqueness so much as relocate it to first-come-first-served with no +; recourse for a losing claim. Revisit only if a real, concrete need for +; unbounded third-party-free resolution emerges. +; core/exec — process and PTY control, instantiating the generic streaming +; pattern from streaming.cddl. Control verbs travel as manage-command, never +; as dedicated frame kinds; only stdio gets dedicated stream frames — this +; split matches Cascade's own exec design exactly. The scope a write/resize/ +; kill/signal is authorised over is the scope its session was spawned under, +; resolved from the receiving node's own state, not the scope carried on the +; wire — a caller cannot drive a session outside its own grant by lying in a +; request's scope field. That check is a verifier obligation, not something +; this schema can enforce structurally. + +$manage-command-params /= pty-spawn / pty-write / pty-resize / pty-kill / proc-spawn / proc-signal / proc-kill / exec-list + +pty-spawn = { verb: "pty.spawn", ? shell: tstr, argv: [* tstr], ? cwd: tstr, env: {* tstr => tstr}, cols: uint, rows: uint } +pty-write = { verb: "pty.write", session: stream-session, bytes: bstr } +pty-resize = { verb: "pty.resize", session: stream-session, cols: uint, rows: uint } +pty-kill = { verb: "pty.kill", session: stream-session, signal: int } +proc-spawn = { verb: "proc.spawn", argv: [* tstr], ? cwd: tstr, env: {* tstr => tstr} } +proc-signal = { verb: "proc.signal", session: stream-session, signal: int } +proc-kill = { verb: "proc.kill", session: stream-session } + +; Session enumeration — a first-class part of the exec capability, not an +; afterthought: a node tracks its own live sessions and can list them for an +; authorised peer, scoped the same way any other exec verb is. (A synthetic +; /proc-style presentation of running sessions is optional sugar on top of +; this; enumeration itself is not.) +exec-list = { verb: "exec.list" } +exec-session-info = { session: stream-session, kind: "pty" / "proc", ? argv: [* tstr], ? cwd: tstr } +; A manage-response-frame answering exec.list carries manage-ok extended with +; `sessions: [* exec-session-info]`, using manage-ok's own open {* tstr => any} +; tail rather than a dedicated result type. +; core/federation — selective, explicit sharing between independently +; operated meshes, distinct from ordinary intra-mesh relay. No Cascade +; precedent exists for this; it is designed as its own capability domain +; rather than a separate connection type, so it reuses Handshake negotiation, +; the Frame choice, and capability-scope instead of inventing a parallel +; mechanism. Generalises agent-comms' own existing fed_handshake/fed_room_* +; messages past one specific application. +; +; Two designated gateway devices, one per mesh, negotiate core/federation at +; Handshake like any other domain, then speak these frames on that +; connection. + +mesh-id = tstr ; a human-chosen or hash-derived identifier for an independently-operated mesh; opaque, not required to be device-id-shaped + +federation-link-request-frame = { + type: "federation-link-request", + local-mesh: mesh-id, + local-name: tstr, + offered-shares: [* share-descriptor], +} + +federation-link-accept-frame = { + type: "federation-link-accept", + remote-mesh: mesh-id, + remote-name: tstr, + accepted-shares: [* share-descriptor], +} + +federation-link-reject-frame = { type: "federation-link-reject", reason: tstr } + +; What's shared: reuses capability-scope so "a room path" and "a folder path" +; are the same generic concept federation already needs elsewhere in this +; schema. +share-descriptor = { + domain: domain-id, + resource: capability-scope, + direction: "inbound" / "outbound" / "bidirectional", +} + +; Renegotiate shares after the initial link — add or remove a room later +; without tearing down the whole federation link. +federation-share-frame = { type: "federation-share", share: share-descriptor } +federation-unshare-frame = { type: "federation-unshare", share: share-descriptor } + +; Once a share is accepted, content belonging to that shared resource is +; forwarded across the link wrapped in this envelope — deliberately +; content-aware and resource-scoped, unlike relay-data-frame's opaque byte +; pipe. That is the actual distinction between federation and ordinary +; intra-mesh relay: relay-data-frame is payload-blind NAT-traversal plumbing; +; federation-envelope-frame is deliberate, selective, resource-scoped +; cross-mesh sharing. origin-device is carried explicitly because the link's +; own peer identity is the gateway device, not the device inside the remote +; mesh that originated the content. +federation-envelope-frame = { + type: "federation-envelope", + origin-mesh: mesh-id, + origin-device: device-id, + resource: capability-scope, + inner: bstr, ; .cbor frame — a full inner Frame value, CBOR-encoded and embedded +} +; The top-level Frame choice. This is the schema functioning as its own type +; registry: a frame's kind is expressed by CDDL's own tagged-union mechanism, +; not a separate external [type][body] byte header the way XDR-based +; protocols (Cascade's own v1 included) require. +; +; Frame *kinds* stay centrally curated — a closed union, seeded once by the +; core files above — rather than open the way domains and capability verbs +; are. A new frame *shape* is a materially bigger interop commitment than a +; new domain string or a new opaque verb: every implementation's decoder has +; to know the shape to do anything with it at all, where an unknown domain or +; verb can simply be advertised, ignored, or rejected without needing to be +; understood. $frame-variant remains a socket, though, so a real extension — +; Cascade's own excluded core/content domain, or any future third-party +; domain that genuinely needs a new frame shape — can still plug in from its +; own schema file, without editing any of the files above. + +$frame-variant /= handshake-frame +$frame-variant /= ping-frame +$frame-variant /= close-frame +$frame-variant /= gossip-frame +$frame-variant /= candidates-frame +$frame-variant /= sync-punch-frame +$frame-variant /= observed-address-frame +$frame-variant /= relay-offer-frame +$frame-variant /= relay-connect-frame +$frame-variant /= relay-data-frame +$frame-variant /= relay-inbound-frame +$frame-variant /= manage-request-frame +$frame-variant /= manage-response-frame +$frame-variant /= revocation-announce-frame +$frame-variant /= stream-data-frame +$frame-variant /= stream-ack-frame +$frame-variant /= stream-end-frame +$frame-variant /= data-have-frame +$frame-variant /= data-request-frame +$frame-variant /= data-entries-frame +$frame-variant /= federation-link-request-frame +$frame-variant /= federation-link-accept-frame +$frame-variant /= federation-link-reject-frame +$frame-variant /= federation-share-frame +$frame-variant /= federation-unshare-frame +$frame-variant /= federation-envelope-frame + +frame = $frame-variant +; Handshake and the domain registry. +; +; A domain is a capability a node advertises at connect time; the usable set +; for a connection is the intersection of what both sides advertise, taken in +; the domain's own comparable form so the negotiated set is independent of +; the order either side listed its domains in. Unknown domains are dropped, +; never assumed to mean anything. +; +; The registry is three-tier, modelled on DNS/reverse-package-name ownership +; rather than a central integer allocator — the only mechanism that scales to +; arbitrary third parties adding domains with no collision and no gatekeeper. + +protocol-version = uint + +domain-id = core-domain-name / namespaced-domain-id / private-use-domain-id + +; Spec-owned, append-only — see registry/core-domains.md. A retired entry is +; marked retired there, never renumbered or reused for something else. +core-domain-name = "core/management" / "core/exec" / "core/data" / "core/federation" + +; "/", where uniqueness comes from the registrant +; segment being a DNS name the registrant already owns — the same trick URNs +; and Java package names use. No wire-mesh authority allocates these. +namespaced-domain-id = tstr .regexp "[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+/[A-Za-z0-9_.-]+" + +; Private-use escape hatch: valid syntax, globally meaningless, must never be +; advertised across an independently operated mesh boundary. +private-use-domain-id = tstr .regexp "x-[A-Za-z0-9_.-]+" + +; The Handshake is the first frame sent on a connection, immediately after +; transport-layer authentication and before any other frame. There is no +; wire-level bound on how many domains may be listed; an implementation may +; impose its own receive-side cap, but that cap is not part of the wire +; contract. +handshake-frame = { + type: "handshake", + version: protocol-version, + domains: [* domain-id], + ? params: {* tstr => any}, ; forward-compatible extension bag, e.g. a future max-frame-size or supported COSE algorithm list +} +; Identity — a node's identity is the hash of its own public key, never the +; certificate that carries it. Hashing the certificate DER (which embeds a +; serial number and a validity window that change on every reissue, even with +; an identical key) was a bug found independently in both Cascade and +; agent-comms; this rule exists to make the correct derivation the only one +; representable. + +identity-key = { + alg: int, ; COSE algorithm identifier (RFC 9053), e.g. -7 ES256, -8 EdDSA + public-key: bstr, ; raw public-key bytes in alg's own canonical encoding +} + +device-id = bstr .size 32 ; SHA-256(identity-key.public-key) — never sha256(certificate DER) + +peer-identity = { + device-id: device-id, + identity-key: identity-key, + ? certificate: bstr, ; optional X.509 DER, present only if the transport binding uses TLS; device-id is always derived from identity-key, regardless of whether a certificate is present +} +; The management domain — a verb command set dispatched by scope and +; capability token, gated by per-command authorisation and audited. +; `$manage-command-params` is a socket, not a closed union: a capability +; domain that needs its own management verbs (core/exec's pty.*/proc.* +; commands, or any future domain) extends it from its own schema file rather +; than editing this one. + +manage-command = { verb: capability-verb, params: $manage-command-params } +$manage-command-params = {* tstr => any} + +manage-request-frame = { + type: "manage-request", + request-id: uint, + command: manage-command, + scope: capability-scope, + ? token: capability-token, +} + +manage-ok = { result: "ok", * tstr => any } +manage-error = { result: "error", code: tstr, ? message: tstr } +manage-response-frame = { type: "manage-response", request-id: uint, outcome: manage-ok / manage-error } + +; Revocation — new, not present in Cascade's frozen set. Gossiped revocation +; entries let a peer check a token against a shared revocation view without +; a synchronous lookup against the issuer for every use of the token. +revocation-entry = { token-id: bstr, revoked-at: uint } +revocation-announce-frame = { type: "revocation-announce", entries: [* revocation-entry] } +; The generic streaming/backpressure pattern — reusable by any capability +; domain that carries a live byte stream (core/exec's stdio today; a future +; audio stream, or a different agent-comms transport, tomorrow), so +; backpressure is a property of the protocol, not something each domain +; reinvents. A consumer advertises a credit window; the producer must never +; send past (ack-seq + window), so a slow consumer throttles the producer +; rather than the node buffering unboundedly. + +stream-session = uint ; opaque, scoped to one connection + +stream-data-frame = { + type: "stream-data", + session: stream-session, + seq: uint, ; per-session monotonic, for ordering and ack + channel: tstr, ; domain-defined tag, e.g. core/exec's "stdin"/"stdout"/"stderr" — each domain owns its own channel vocabulary + bytes: bstr, +} + +stream-ack-frame = { + type: "stream-ack", + session: stream-session, + ack-seq: uint, ; highest contiguous sequence accepted + window: uint, ; credit, in bytes, the consumer will accept past ack-seq +} + +; Sent exactly once, by whichever side owns the stream's lifecycle, on the +; session's terminal exit. Not credit-gated: it carries no sequence number +; and is routed to the stream's registered consumer without an ack. +stream-end-frame = { + type: "stream-end", + session: stream-session, + ? exit-code: int, + ? exit-signal: int, +} +; Capability verbs, scopes, and signed capability tokens. +; +; Authority is a verb over a scope, carried between nodes as a token signed by +; the issuing node's own key. A bearer presents a token; the verifier checks +; the signature, expiry, and revocation before authorising the carried grant. +; Delegation forms a chain where each hop can only narrow authority, never +; widen it, and a token's expiry is clamped to its parent's — that narrowing +; rule is a verifier obligation, not something CDDL itself can enforce. + +capability-verb = core-capability / namespaced-capability / private-use-capability + +; Bare "subsystem:action", spec-owned registry — see registry/core-capabilities.md. +core-capability = tstr .regexp "[a-z][a-z0-9-]*:[a-z][a-z0-9-]*" + +; "/:", same registrant-owned namespace as +; namespaced-domain-id. +namespaced-capability = tstr .regexp "[a-z0-9.-]+/[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+" + +private-use-capability = tstr .regexp "x-[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+" + +; A hierarchical scope string. "kind" is open on purpose: "node" and "folder" +; are Cascade's own scopes; "room" and "org" are agent-comms'; "handle-registry" +; is discovery.cddl's federation-scoped handle-resolution alternative to DNS; +; a future application mints its own kind rather than needing this schema to +; change. +capability-scope = { + kind: tstr, + ? path: tstr, ; absent means the kind's own whole-scope root +} + +cose-header-alg = 1 ; RFC 9052 common header label: algorithm +cose-header-kid = 4 ; RFC 9052 common header label: key identifier + +cose-header-label = int / tstr + +cose-token-headers = { + ? cose-header-alg => int, + ? cose-header-kid => bstr, ; issuer device-id, a fast lookup hint before full verification, not itself trusted + * cose-header-label => any, +} + +; The literal COSE_Sign1 array (RFC 9052 §4.2) rather than a bespoke named +; structure, so every implementation can verify and sign using an off-the- +; shelf COSE library instead of hand-rolling the Sig_structure construction. +cose-sign1 = [ + protected: bstr, ; bstr .cbor cose-token-headers + unprotected: cose-token-headers, + payload: bstr / nil, ; bstr .cbor token-claims, present (not nil) for a capability token + signature: bstr, +] + +capability-token = cose-sign1 + +; The signed payload. issuer-key travels inside the signed content, not as a +; sibling of the COSE envelope, specifically so the token is self-certifying: +; a verifier checks that sha256(issuer-key.public-key) equals the claimed +; issuer device-id, and that the signature verifies against that same +; embedded key — no shared secret or prior contact with the issuer is needed +; to verify a token, only the token itself. +token-claims = { + token-id: bstr, + issuer: device-id, + issuer-key: identity-key, + bearer: device-id, + capability: capability-verb, + scope: capability-scope, + expires: uint, ; Unix ms + ? not-before: uint, + ? parent: bstr, ; bstr .cbor capability-token — a fully self-contained, nested COSE_Sign1 of the parent token; the recursive delegation chain + * tstr => any, ; forward-compatible extension claims +} + +; Signing convention (a verifier obligation, not expressible as a CDDL +; constraint): `signature` is computed per RFC 9052 §4.4's Sig_structure, +; the array ["Signature1", protected, external_aad, payload], where +; external_aad is the empty bstr and payload is the canonical DAG-CBOR +; encoding of token-claims. Canonical encoding is a codec-level guarantee +; this schema cannot itself enforce — every implementation's CBOR codec must +; produce the DAG-CBOR canonical form for token-claims specifically, or +; signatures will not verify across implementations even when both are +; individually correct. +; Transport frames — domain-independent; every peer speaks these regardless +; of the negotiated capability set, since they carry the connection itself +; rather than any one domain's content. Field layouts transcribed directly +; from Cascade's own BepMessage/Candidate/CandidateKind types, re-expressed +; in CDDL rather than XDR. +; +; Cascade's own `content` domain (BEP block exchange: ClusterConfig, Index, +; IndexUpdate, Request, Response) is deliberately out of scope here — it has +; no analogue in agent-comms and doesn't fit any of wire-mesh's generic +; patterns. It remains Cascade's own extension, pluggable into the +; $frame-variant socket (see frame.cddl) from Cascade's own schema file, +; never part of the core protocol. + +ping-frame = { type: "ping" } +close-frame = { type: "close", ? reason: tstr } + +peer-advert = { + device: device-id, + addresses: [* tstr], ; "host:port" strings + snapshot-seconds: int, ; Unix-seconds snapshot time +} +gossip-frame = { type: "gossip", peers: [* peer-advert] } + +candidate-kind = "host" / "server-reflexive" / "relayed" +wire-candidate = { address: tstr, kind: candidate-kind, priority: uint } ; address = "ip:port" +candidates-frame = { type: "candidates", candidates: [* wire-candidate] } + +sync-punch-frame = { type: "sync-punch", nonce: uint, deadline-unix-ms: uint } +observed-address-frame = { type: "observed-address", address: tstr } + +; Relay: when two peers cannot connect directly, a relay carries their +; traffic as an opaque, unreadable byte pipe. The relay never holds the keys +; to decrypt what it forwards — relay-data-frame's payload is ciphertext +; established one layer above the transport, not something this frame +; interprets. +relay-offer-frame = { type: "relay-offer", addresses: [* tstr] } +relay-connect-frame = { type: "relay-connect", target-device: device-id } +relay-data-frame = { type: "relay-data", payload: bstr } +relay-inbound-frame = { type: "relay-inbound", source-device: device-id } diff --git a/test/fixtures/tokens.v1.json b/test/fixtures/tokens.v1.json new file mode 100644 index 0000000..0cf5067 --- /dev/null +++ b/test/fixtures/tokens.v1.json @@ -0,0 +1,54 @@ +{ + "protocol_version": 1, + "description": "Capability-token and handle-record conformance vectors for protocol version 1. Every cose-sign1 array's protected/payload byte strings are themselves canonical CBOR, decoded and re-verified the same way as any other bstr field. Signature and public-key bytes are structural placeholders (clearly-synthetic filler), not real cryptographic material -- this file freezes the byte-exact envelope shape (map key ordering, field presence, the recursive parent delegation chain), not a working signature, the same scope Cascade's own frozen vectors commit to for fields with no real crypto behind them yet.", + "vectors": [ + { + "name": "capability_token_v1_root_grant", + "message": [ + { + "hex": "a2613126613458201111111111111111111111111111111111111111111111111111111111111111" + }, + {}, + { + "hex": "a76573636f7065a2646b696e6466666f6c6465726470617468652f776f726b6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450010101010101010101010101010101016a6361706162696c69747968657865633a7074796a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + { + "hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "wire_hex": "845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a059010fa76573636f7065a2646b696e6466666f6c6465726470617468652f776f726b6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450010101010101010101010101010101016a6361706162696c69747968657865633a7074796a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "name": "capability_token_v1_delegated_narrowed_scope", + "message": [ + { + "hex": "a2613126613458202222222222222222222222222222222222222222222222222222222222222222" + }, + {}, + { + "hex": "a86573636f7065a2646b696e6466666f6c64657264706174686c2f776f726b2f7375626469726662656172657258203333333333333333333333333333333333333333333333333333333333333333666973737565725820222222222222222222222222222222222222222222222222222222222222222266706172656e74590180845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a059010fa76573636f7065a2646b696e6466666f6c6465726470617468652f776f726b6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450010101010101010101010101010101016a6361706162696c69747968657865633a7074796a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff67657870697265731b000001b18314880068746f6b656e2d696450020202020202020202020202020202026a6361706162696c69747968657865633a7074796a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + { + "hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "wire_hex": "845828a2613126613458202222222222222222222222222222222222222222222222222222222222222222a05902a0a86573636f7065a2646b696e6466666f6c64657264706174686c2f776f726b2f7375626469726662656172657258203333333333333333333333333333333333333333333333333333333333333333666973737565725820222222222222222222222222222222222222222222222222222222222222222266706172656e74590180845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a059010fa76573636f7065a2646b696e6466666f6c6465726470617468652f776f726b6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450010101010101010101010101010101016a6361706162696c69747968657865633a7074796a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff67657870697265731b000001b18314880068746f6b656e2d696450020202020202020202020202020202026a6361706162696c69747968657865633a7074796a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "name": "handle_record_v1_dns_anchored", + "message": [ + { + "hex": "a1613127" + }, + {}, + { + "hex": "a66668616e646c6571616c696365406578616d706c652e636f6d666973737565641b000001b17dee2c0067657870697265731b000001b183148800696465766963652d6964582044444444444444444444444444444444444444444444444444444444444444446a63616e6469646174657381a3646b696e6464686f73746761646472657373703230332e302e3131332e353a34343333687072696f7269747918646c6964656e746974792d6b6579a263616c67276a7075626c69632d6b65795820eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + { + "hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "wire_hex": "8444a1613127a058e2a66668616e646c6571616c696365406578616d706c652e636f6d666973737565641b000001b17dee2c0067657870697265731b000001b183148800696465766963652d6964582044444444444444444444444444444444444444444444444444444444444444446a63616e6469646174657381a3646b696e6464686f73746761646472657373703230332e302e3131332e353a34343333687072696f7269747918646c6964656e746974792d6b6579a263616c67276a7075626c69632d6b65795820eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ] +} diff --git a/test/round-trip.test.ts b/test/round-trip.test.ts new file mode 100644 index 0000000..7269d09 --- /dev/null +++ b/test/round-trip.test.ts @@ -0,0 +1,122 @@ +// Proves cddl.js's generated schemas against real-world ground truth: for every conformance vector wire-mesh records, the vector's raw CBOR bytes decode into something the corresponding generated Zod schema accepts, and re-encoding the validated value reproduces the original bytes exactly. This is cddl.js's own definition of done, not merely a smoke test. + +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { beforeAll, describe, expect, it } from "vitest"; +import { decode, encode, cdeDecodeOptions, cdeEncodeOptions } from "cbor2"; +import { emitModule } from "../src/emitter.ts"; +import { parse } from "../vendor/cddl/dist/index.js"; + +interface Vector { + name: string; + wire_hex: string; +} + +interface VectorFile { + vectors: Vector[]; +} + +function isVectorFile(value: unknown): value is VectorFile { + if (typeof value !== "object" || value === null) return false; + if (!("vectors" in value) || !Array.isArray(value.vectors)) return false; + return value.vectors.every( + (v: unknown) => + typeof v === "object" && + v !== null && + "name" in v && + "wire_hex" in v && + typeof v.name === "string" && + typeof v.wire_hex === "string", + ); +} + +function readVectors(filename: string): Vector[] { + const path = fileURLToPath(new URL(`fixtures/${filename}`, import.meta.url)); + const raw: unknown = JSON.parse(readFileSync(path, "utf8")); + if (!isVectorFile(raw)) { + throw new Error(`${filename} is not a valid vector file`); + } + return raw.vectors; +} + +interface GeneratedSchemas { + frameSchema: { + safeParse: (value: unknown) => { success: boolean; data?: unknown }; + }; + capabilityTokenSchema: { + safeParse: (value: unknown) => { success: boolean; data?: unknown }; + }; +} + +function isGeneratedSchemas(value: unknown): value is GeneratedSchemas { + if (typeof value !== "object" || value === null) return false; + return "frameSchema" in value && "capabilityTokenSchema" in value; +} + +let schemas: GeneratedSchemas; + +beforeAll(async () => { + const cddlPath = fileURLToPath( + new URL("fixtures/protocol.cddl", import.meta.url), + ); + const parsed = parse(cddlPath); + const source = emitModule(parsed); + + const generatedDir = fileURLToPath( + new URL("fixtures/generated/", import.meta.url), + ); + mkdirSync(generatedDir, { recursive: true }); + const generatedUrl = new URL( + "fixtures/generated/protocol.ts", + import.meta.url, + ); + writeFileSync(fileURLToPath(generatedUrl), source); + + // Imported via a runtime URL value, not a literal specifier: the file doesn't exist until the writeFileSync above creates it, so tsc must not try to statically resolve it (a literal "./fixtures/generated/protocol.ts" specifier fails typecheck on a clean checkout, before any test run has ever generated the file). + const imported: unknown = await import(generatedUrl.href); + if (!isGeneratedSchemas(imported)) { + throw new Error( + "generated module is missing frameSchema/capabilityTokenSchema", + ); + } + schemas = imported; +}); + +function roundTrip( + vector: Readonly, + schema: GeneratedSchemas["frameSchema"], +): void { + // A plain Uint8Array, not a Node Buffer: cbor2 slices embedded byte strings via the input's own subarray(), which on a Buffer returns another Buffer -- a constructor cbor2's encoder doesn't recognise as a byte string when it comes time to re-encode, producing a garbled {type, data} map instead (the same gotcha conformance/codec.ts documents for the reverse direction). + const bytes = Uint8Array.from(Buffer.from(vector.wire_hex, "hex")); + const decoded: unknown = decode(bytes, cdeDecodeOptions); + + const result = schema.safeParse(decoded); + expect(result.success, `schema rejected vector "${vector.name}"`).toBe(true); + + const reEncoded = Buffer.from(encode(result.data, cdeEncodeOptions)).toString( + "hex", + ); + expect( + reEncoded, + `re-encoding "${vector.name}" did not reproduce wire_hex`, + ).toBe(vector.wire_hex); +} + +describe("frame vectors decode and re-encode byte-exactly through frameSchema", () => { + for (const vector of [ + ...readVectors("frames.v1.json"), + ...readVectors("handshake.v1.json"), + ]) { + it(vector.name, () => { + roundTrip(vector, schemas.frameSchema); + }); + } +}); + +describe("token vectors decode and re-encode byte-exactly through capabilityTokenSchema", () => { + for (const vector of readVectors("tokens.v1.json")) { + it(vector.name, () => { + roundTrip(vector, schemas.capabilityTokenSchema); + }); + } +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..da80642 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/emitter.ts", "src/runtime.ts"] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..aff3744 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true + }, + "include": ["src/cli.ts", "tsdown.config.ts", "eslint.config.ts", "test/**/*.ts"], + "exclude": ["test/fixtures/generated/protocol.ts"] +} diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 0000000..fe31364 --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsdown"; + +// src/emitter.ts (emitModule/mergeRules) and src/runtime.ts (cborDecodesAs) are the library's public surface, built directly as separate entries rather than through a re-exporting index.ts -- the project's barrel-policy lint rule requires importing straight from the module that owns each export. Each is built dual ESM/CJS with declarations so it resolves correctly under every module system; attw verifies that claim directly rather than trusting it. src/cli.ts (a script, not library surface) and vendor/cddl (compiled separately, via its own tsc step -- see package.json's _vendor:build) are deliberately not part of this build. +export default defineConfig({ + entry: ["src/emitter.ts", "src/runtime.ts"], + format: ["esm", "cjs"], + dts: true, + exports: true, + attw: { profile: "node16" }, + clean: true, +}); diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..a27d34e --- /dev/null +++ b/turbo.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://turborepo.com/schema.json", + + // Every task name is the underscore-prefixed one the package.json script actually runs, matching wire-mesh's conformance/ convention: a task literally named "build" would make `pnpm run build`'s own `turbo run build` call resolve straight back to itself. + "tasks": { + "_vendor:build": { + "inputs": ["vendor/cddl/src/**", "vendor/cddl/tsconfig.json"], + "outputs": ["vendor/cddl/dist/**"] + }, + "_build": { + "inputs": ["src/index.ts", "src/emitter.ts", "src/runtime.ts", "tsdown.config.ts", "tsconfig.json"], + "outputs": ["dist/**"] + }, + "_generate": { + "dependsOn": ["_build", "_vendor:build"], + "inputs": ["src/cli.ts"], + "outputs": [] + }, + "_test": { + "dependsOn": ["_build", "_vendor:build"] + }, + "_typecheck": { + "dependsOn": ["_build", "_vendor:build"], + "inputs": ["**/*.ts", "tsconfig.json", "tsconfig.node.json"] + }, + "_lint": { + "dependsOn": ["_build"], + "inputs": ["$TURBO_DEFAULT$", "eslint.config.ts"], + "outputs": [".eslintcache"] + } + } +} diff --git a/vendor/cddl/LICENSE b/vendor/cddl/LICENSE new file mode 100644 index 0000000..ebfe373 --- /dev/null +++ b/vendor/cddl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 WebdriverIO Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/cddl/README.md b/vendor/cddl/README.md new file mode 100644 index 0000000..94b8dfe --- /dev/null +++ b/vendor/cddl/README.md @@ -0,0 +1,22 @@ +# vendor/cddl/ + +A vendored copy of [webdriverio/cddl](https://github.com/webdriverio/cddl)'s `packages/cddl/src` (the CDDL parser this project's [foundation decision](../../docs/0001-foundation.md) is built on), MIT-licensed, unmodified except for three upstream fixes cddl.js needs that aren't released yet: + +- `fix: operators on array members no longer crash the parser` -- [webdriverio/cddl#88](https://github.com/webdriverio/cddl/pull/88), needs a maintainer to sign the project's EasyCLA before it can merge. +- `feat: add .cbor and .cborseq control operators` -- stacked on the above, tracking [webdriverio/cddl#89](https://github.com/webdriverio/cddl/issues/89), PR at [Mearman/cddl#1](https://github.com/Mearman/cddl/pull/1). +- `fix: ? occurrence indicator no longer parses as unbounded` -- stacked on the above two, tracking [webdriverio/cddl#90](https://github.com/webdriverio/cddl/issues/90), PR at [Mearman/cddl#2](https://github.com/Mearman/cddl/pull/2). `?` and `*` shared the same default upper bound (`Infinity`), so an arrow-syntax entry keyed by a rule reference (`? cose-header-alg => int`) and the generic open-map-tail pattern (`* tstr => any`) parsed with an identical occurrence, making them indistinguishable -- found while generating Zod schemas against wire-mesh's real `cose-token-headers` rule. + +All three are recorded in full, with their own test coverage, in [docs/0002-spike.md](../../docs/0002-spike.md) and the commits on [Mearman/cddl](https://github.com/Mearman/cddl) (`fix/array-member-operators`, `feat/cbor-operator`, `fix/optional-occurrence-defaults-to-one`). + +## Why vendored, not a dependency + +cddl.js's foundation decision commits to attempting the fix upstream first, with a vendored fork as the fallback -- not because upstreaming failed, but because it needs a maintainer's action (the CLA) this project has no control over the timing of, and cddl.js's own work (the actual point of this repository) shouldn't block on that. + +## Removing this once upstream ships + +Once webdriverio/cddl#88 and the `.cbor` PR both merge and a release goes out: + +1. Delete this directory. +2. Add `cddl` as an ordinary npm dependency instead. +3. Update the one import site (`src/parse.ts`) from `../vendor/cddl/src/index.js` to `cddl`. +4. Delete this README and the two decision-record entries' "vendored, not yet upstream" framing (the decisions themselves -- pure-JS foundation, the two operator fixes -- don't change; only where the code lives does). diff --git a/vendor/cddl/src/ast.ts b/vendor/cddl/src/ast.ts new file mode 100644 index 0000000..6fca0ed --- /dev/null +++ b/vendor/cddl/src/ast.ts @@ -0,0 +1,183 @@ +/** + * a group definition + * ``` + * person = { + * age: int, + * name: tstr, + * employer: tstr, + * } + * ``` + */ +export type Group = { + Type: 'group' + Name: string + IsChoiceAddition: boolean + Properties: (Property|Property[])[] + Comments: Comment[] +} + +/** + * an array definition + * ``` + * Geography = [ + * city: tstr + * ] + * ``` + */ +export type Array = { + Type: 'array' + Name: string + Values: (Property|Property[])[] + Comments: Comment[] +} + +/** + * a tag definition + * ``` + * #6.32(tstr) + * ``` + */ +export type Tag = { + NumericPart: number + TypePart: string +} + +/** + * a variable assignment + * ``` + * device-address = byte + * ``` + */ +export type Variable = { + Type: 'variable' + Name: string + IsChoiceAddition: boolean + PropertyType: PropertyType | PropertyType[] + Operator?: Operator + Comments: Comment[] +} + +/** + * a comment statement + * ``` + * ; This is a comment + * ``` + */ +export type Comment = { + Type: 'comment' + Content: string + Leading: boolean +} + +export type Occurrence = { + n: number + m: number +} + +export type Property = { + HasCut: boolean + Occurrence: Occurrence + Name: PropertyName + Type: PropertyType | PropertyType[] + Comments: Comment[] + Operator?: Operator +} + +export enum Type { + /** + * any types + */ + ANY = 'any', + + /** + * boolean types + */ + // Boolean value (major type 7, additional information 20 or 21). + BOOL = 'bool', + + /** + * numeric types + */ + // An unsigned integer or a negative integer. + INT = 'int', + // An unsigned integer (major type 0). + UINT = 'uint', + // A negative integer (major type 1). + NINT = 'nint', + // One of float16, float32, or float64. + FLOAT = 'float', + // A number representable as an IEEE 754 half-precision float + FLOAT16 = 'float16', + // A number representable as an IEEE 754 single-precision + FLOAT32 = 'float32', + // A number representable as an IEEE 754 double-precision + FLOAT64 = 'float64', + + /** + * string types + */ + // A byte string (major type 2). + BSTR = 'bstr', + // A byte string (major type 2). + BYTES = 'bytes', + // Text string (major type 3) + TSTR = 'tstr', + // Text string (major type 3) + TEXT = 'text', + + /** + * null types + */ + NIL = 'nil', + NULL = 'null' +} + +/** + * can be a number, e.g. "foo = 0..10" + * ``` + * { + * Type: "int", + * Value: 6 + * } + * ``` + * or a literal, e.g. "foo = 0..max-byte" + * ``` + * { + * Type: "literal", + * Value: "max-byte" + * } + * ``` + */ +export type RangePropertyReference = number | string + +export type Range = { + Min: RangePropertyReference + Max: RangePropertyReference + Inclusive: boolean +} + +export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'cbor' | 'cborseq' +export interface Operator { + Type: OperatorType + Value: PropertyType +} + +export type PropertyReferenceType = 'literal' | 'group' | 'group_array' | 'array' | 'range' | 'tag' +export type PropertyReference = { + Type: PropertyReferenceType + Value: string | number | boolean | Group | Array | Range | Tag + Unwrapped: boolean + Operator?: Operator + // `true` when a numeric literal was written as a float (e.g. `1.0`), so a + // whole-valued float range `(0.0..1.0)` stays distinct from `(0..1)` + IsFloat?: boolean +} + +export interface NativeTypeWithOperator { + Type: Type | PropertyReference + Operator?: Operator +} + +export type Assignment = Group | Array | Variable +export type PropertyType = Assignment | Array | PropertyReference | string | NativeTypeWithOperator +export type PropertyName = string diff --git a/vendor/cddl/src/constants.ts b/vendor/cddl/src/constants.ts new file mode 100644 index 0000000..ba0a6e5 --- /dev/null +++ b/vendor/cddl/src/constants.ts @@ -0,0 +1,16 @@ +export const WHITESPACE_CHARACTERS = [' ', '\t', '\n', '\r'] +export const BOOLEAN_LITERALS = ['true', 'false'] + +/** + * as defined in Appendix D + * https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#appendix-D + */ +export const PREDEFINED_IDENTIFIER = [ + 'any', 'uint', 'nint', 'int', 'bstr', 'bytes', 'tstr', 'text', + 'tdate', 'time', 'number', 'biguint', 'bignint', 'bigint', + 'integer', 'unsigned', 'decfrac', 'bigfloat', 'eb64url', + 'eb64legacy', 'eb16', 'encoded-cbor', 'uri', 'b64url', + 'b64legacy', 'regexp', 'mime-message', 'cbor-any', 'float16', + 'float32', 'float64', 'float16-32', 'float32-64', 'float', + 'false', 'true', 'bool', 'nil', 'null', 'undefined' +] diff --git a/vendor/cddl/src/index.ts b/vendor/cddl/src/index.ts new file mode 100644 index 0000000..42e745a --- /dev/null +++ b/vendor/cddl/src/index.ts @@ -0,0 +1,12 @@ +import Lexer from './lexer.js' +import Parser from './parser.js' + +export function parse (filePath: string) { + const parser = new Parser(filePath) + return parser.parse() +} + +export default { parse } +export { Lexer, Parser } +export * from './ast.js' +export * from './utils.js' diff --git a/vendor/cddl/src/lexer.ts b/vendor/cddl/src/lexer.ts new file mode 100644 index 0000000..b3cd311 --- /dev/null +++ b/vendor/cddl/src/lexer.ts @@ -0,0 +1,238 @@ +import { Token, Tokens } from './tokens.js'; +import { isLetter, isAlphabeticCharacter, isDigit, hasSpecialNumberCharacter } from './utils.js' +import { WHITESPACE_CHARACTERS } from './constants.js'; + +export default class Lexer { + input: string + position: number = 0 + readPosition: number = 0 + ch: number = 0 + + constructor (source: string) { + this.input = source + + this.readChar() + } + + private readChar (): void { + if (this.readPosition >= this.input.length) { + this.ch = 0 + } else { + this.ch = this.input[this.readPosition].charCodeAt(0) + } + this.position = this.readPosition + this.readPosition++ + } + + getLocation () { + const position = this.position - 2 + const sourceLines = this.input.split('\n') + const sourceLineLength = sourceLines.map((l) => l.length) + let i = 0 + + for (const [line, lineLength] of Object.entries(sourceLineLength)) { + i += lineLength + 1 + if (i > position) { + const lineBegin = i - lineLength + return { + line: parseInt(line, 10), + position: position - lineBegin + 1 + } + } + } + + return { line: 0, position: 0 } + } + + getLine (lineNumber: number) { + return this.input.split('\n')[lineNumber] + } + + getLocationInfo () { + const loc = this.getLocation() + const line = loc ? this.getLine(loc.line) : '' + let locationInfo = line + '\n' + locationInfo += ' '.repeat(loc?.position || 0) + '^\n' + locationInfo += ' '.repeat(loc?.position || 0) + '|\n' + return locationInfo + } + + nextToken (): Token { + let token: Token + this.skipWhitespace() + + const Literal = String.fromCharCode(this.ch) + switch (this.ch) { + case '='.charCodeAt(0): + token = { Type: Tokens.ASSIGN, Literal } + break + case '('.charCodeAt(0): + token = { Type: Tokens.LPAREN, Literal } + break + case ')'.charCodeAt(0): + token = { Type: Tokens.RPAREN, Literal } + break + case '{'.charCodeAt(0): + token = { Type: Tokens.LBRACE, Literal } + break + case '}'.charCodeAt(0): + token = { Type: Tokens.RBRACE, Literal } + break + case '['.charCodeAt(0): + token = { Type: Tokens.LBRACK, Literal } + break + case ']'.charCodeAt(0): + token = { Type: Tokens.RBRACK, Literal } + break + case '<'.charCodeAt(0): + token = { Type: Tokens.LT, Literal } + break + case '>'.charCodeAt(0): + token = { Type: Tokens.GT, Literal } + break + case '+'.charCodeAt(0): + token = { Type: Tokens.PLUS, Literal } + break + case ','.charCodeAt(0): + token = { Type: Tokens.COMMA, Literal } + break + case '.'.charCodeAt(0): + token = { Type: Tokens.DOT, Literal } + break + case ':'.charCodeAt(0): + token = { Type: Tokens.COLON, Literal } + break + case '?'.charCodeAt(0): + token = { Type: Tokens.QUEST, Literal } + break + case '/'.charCodeAt(0): + token = { Type: Tokens.SLASH, Literal } + break + case '*'.charCodeAt(0): + token = { Type: Tokens.ASTERISK, Literal } + break + case '^'.charCodeAt(0): + token = { Type: Tokens.CARET, Literal } + break + case '#'.charCodeAt(0): + token = { Type: Tokens.HASH, Literal } + break + case '~'.charCodeAt(0): + token = { Type: Tokens.TILDE, Literal } + break + case '"'.charCodeAt(0): + token = { Type: Tokens.STRING, Literal: this.readString() } + break + case ';'.charCodeAt(0): + token = { Type: Tokens.COMMENT, Literal: this.readComment() } + break + case 0: + token = { Type: Tokens.EOF, Literal: '' } + break + default: { + if (isAlphabeticCharacter(Literal)) { + return { Type: Tokens.IDENT, Literal: this.readIdentifier() } + } else if ( + // positive number + isDigit(Literal) || + // negative number + (this.ch === Tokens.MINUS.charCodeAt(0) && isDigit(this.input[this.readPosition])) + ) { + const numberOrFloat = this.readNumberOrFloat() + return { + Type: numberOrFloat.includes(Tokens.DOT) ? Tokens.FLOAT : Tokens.NUMBER, + Literal: numberOrFloat + } + } + token = { Type: Tokens.ILLEGAL, Literal: '' } + } + } + + this.readChar() + return token + } + + private readIdentifier (): string { + const position = this.position + + /** + * an identifier can contain + * see https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#section-3.1 + */ + while ( + // a letter (a-z, A-Z) + isLetter(String.fromCharCode(this.ch)) || + // a digit (0-9) + isDigit(String.fromCharCode(this.ch)) || + // and special characters (-, _, @, ., $) + [ + Tokens.MINUS.charCodeAt(0), + Tokens.UNDERSCORE.charCodeAt(0), + Tokens.ATSIGN.charCodeAt(0), + Tokens.DOT.charCodeAt(0), + Tokens.DOLLAR.charCodeAt(0) + ].includes(this.ch) + ) { + this.readChar() + } + + return this.input.slice(position, this.position) + } + + private readComment (): string { + const position = this.position + + while (this.ch && String.fromCharCode(this.ch) !== '\n') { + this.readChar() + } + + return this.input.slice(position, this.position).trim() + } + + private readString (): string { + const position = this.position + + this.readChar() // eat " + while (this.ch && String.fromCharCode(this.ch) !== Tokens.QUOT) { + this.readChar() // eat any character until " + } + + return this.input.slice(position + 1, this.position).trim() + } + + private readNumberOrFloat (): string { + const position = this.position + let foundSpecialCharacter = false + + /** + * a number of float can contain + */ + while ( + // a number + isDigit(String.fromCharCode(this.ch)) || + // a special character, e.g. ".", "x" and "b" + hasSpecialNumberCharacter(this.ch) + ) { + /** + * ensure we respect ranges, e.g. 0..10 + * so break after the second dot and adjust read position + */ + if (hasSpecialNumberCharacter(this.ch) && foundSpecialCharacter) { + this.position-- + this.readPosition-- + break + } + + foundSpecialCharacter = hasSpecialNumberCharacter(this.ch) + this.readChar() // eat any character until a non digit or a 2nd dot + } + + return this.input.slice(position, this.position).trim() + } + + private skipWhitespace () { + while (WHITESPACE_CHARACTERS.includes(String.fromCharCode(this.ch))) { + this.readChar() + } + } +} diff --git a/vendor/cddl/src/parser.ts b/vendor/cddl/src/parser.ts new file mode 100644 index 0000000..a22e66b --- /dev/null +++ b/vendor/cddl/src/parser.ts @@ -0,0 +1,1053 @@ +import fs from 'node:fs' + +import Lexer from './lexer.js' +import { Property } from './ast.js' +import { Token, Tokens } from './tokens.js'; +import { PREDEFINED_IDENTIFIER, BOOLEAN_LITERALS } from './constants.js' +import { parseNumberValue } from './utils.js' +import { + Type, PropertyName, PropertyType, PropertyReferenceType, + Variable, RangePropertyReference, Occurrence, Assignment, + Comment, Group, OperatorType, NativeTypeWithOperator, Operator +} from './ast.js' + +const NIL_TOKEN: Token = { Type: Tokens.ILLEGAL, Literal: '' } +const DEFAULT_OCCURRENCE: Occurrence = { n: 1, m: 1 } // exactly one time +const OPERATORS: OperatorType[] = ['default', 'size', 'regexp', 'bits', 'and', 'within', 'eq', 'ne', 'lt', 'le', 'gt', 'ge', 'cbor', 'cborseq'] +const OPERATORS_EXPECTING_VALUES: Record = { + default: undefined, + size: ['literal', 'range'], + regexp: ['literal'], + bits: ['group'], + and: ['group'], + within: ['group'], + eq: ['group'], + ne: ['group'], + lt: ['group'], + le: ['group'], + gt: ['group'], + ge: ['group'], + cbor: ['group'], + cborseq: ['group'], +} + +export default class Parser { + #filePath: string + l: Lexer; + + curToken: Token = NIL_TOKEN; + peekToken: Token = NIL_TOKEN; + peekBelowToken: Token = NIL_TOKEN; + + constructor (filePath: string) { + this.#filePath = filePath + this.l = new Lexer(fs.readFileSync(filePath, 'utf-8')) + + this.nextToken() + this.nextToken() + this.nextToken() + } + + private nextToken () { + this.curToken = this.peekToken + this.peekToken = this.peekBelowToken + this.peekBelowToken = this.l.nextToken() + return true + } + + private parseAssignments (): Assignment { + const comments: Comment[] = [] + while (this.curToken.Type === Tokens.COMMENT) { + const comment = this.parseComment() + if (comment) { + comments.push(comment) + } + } + + /** + * expect group identifier, e.g. + * groupName = + * groupName /= + * groupName //= + */ + if (this.curToken.Type !== Tokens.IDENT || !(this.peekToken.Type === Tokens.ASSIGN || this.peekToken.Type === Tokens.SLASH)) { + throw this.parserError(`group identifier expected, received "${JSON.stringify(this.curToken)}"`) + } + + let isChoiceAddition = false + const groupName = this.curToken.Literal + this.nextToken() // eat group identifier + + // @ts-ignore + if (this.curToken.Type === Tokens.SLASH) { + isChoiceAddition = true + this.nextToken() // eat `/` + } + + // @ts-ignore + if (this.curToken.Type === Tokens.SLASH) { + this.nextToken() // eat `/` + } + + this.nextToken() // eat `=` + const assignmentValue = this.parseAssignmentValue(groupName, isChoiceAddition) as Assignment + + // @ts-expect-error curToken can be changed by now but TS doesn't understand this + while (this.curToken.Type === Tokens.COMMENT) { + const comment = this.parseComment() + comment && comments.push(comment) + } + assignmentValue.Comments = comments + return assignmentValue + } + + private parseAssignmentValue (groupName?: string, isChoiceAddition = false): Assignment | PropertyType[] { + let isChoice = false + const valuesOrProperties: (Property | Property[])[] = [] + const closingTokens = this.openSegment() + + /** + * if no group segment was opened we have a variable assignment + * and can return immediatelly, e.g. + * + * attire = "bow tie" / "necktie" / "Internet attire" + * + */ + if (closingTokens.length === 0) { + if (groupName) { + const variable: Variable = { + Type: 'variable', + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: this.parsePropertyTypes(true), + Comments: [] + } + + return variable + } + + return this.parsePropertyTypes() + } + + /** + * type or group choices can be wrapped within `(` and `)`, e.g. + * + * attireBlock = ( + * "bow tie" / + * "necktie" / + * "Internet attire" + * ) + * attireGroup = ( + * attire // + * attireBlock + * ) + */ + if ( + closingTokens.includes(Tokens.RPAREN) && + this.peekToken.Type === Tokens.SLASH && + this.peekBelowToken.Type !== Tokens.SLASH && + !(this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) + ) { + const propertyType: PropertyType[] = [] + while (!closingTokens.includes(this.curToken.Type)) { + propertyType.push(...this.parsePropertyTypes(true)) + if (closingTokens.includes(this.curToken.Type)) { + this.nextToken() + break + } + + this.nextToken() + + if (this.curToken.Type === Tokens.SLASH) { + this.nextToken() + } + } + if (this.curToken.Type === Tokens.RPAREN) { + this.nextToken(); + } + if (groupName) { + const variable: Variable = { + Type: 'variable', + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: propertyType, + Comments: [] + } + + if (this.isOperator()) { + variable.Operator = this.parseOperator() + } + + return variable + } + + return propertyType + } + + /** + * parse operator assignments, e.g. `ip4 = (float .ge 0.0) .default 1.0` + */ + if (closingTokens.length === 1 && this.peekToken.Type === Tokens.DOT) { + const propertyType = this.parsePropertyType() + const operator = this.isOperator() ? this.parseOperator() : undefined + const prop: PropertyType = { + Type: propertyType, + ...(operator ? { Operator: operator } : {}) + } as NativeTypeWithOperator + + /** + * this branch exists for the single-element case, e.g. `ip4 = (float .ge 0.0)`, where the operator's value is immediately followed by the closing token. If a comma follows instead, e.g. `[ bstr .size 3, bstr ]`, there are more elements still to come -- seed the general array/group loop below with this first element rather than assuming we are already done. + */ + if (this.curToken.Type === Tokens.COMMA) { + valuesOrProperties.push({ + HasCut: false, + Occurrence: DEFAULT_OCCURRENCE, + Name: '', + Type: [prop], + Comments: [] + }) + this.nextToken() // eat comma + } else { + this.nextToken() // eat closing token + if (groupName) { + const trailingOperator = this.isOperator() ? this.parseOperator() : undefined + const variable: Variable = { + Type: 'variable', + Name: groupName, + IsChoiceAddition: isChoiceAddition, + PropertyType: prop, + ...(trailingOperator ? { Operator: trailingOperator } : {}), + Comments: [] + } + + return variable + } + + return [prop] + } + } + + while (!closingTokens.includes(this.curToken.Type)) { + const comments: Comment[] = [] + let leadingComment = this.parseComment(true) + while (leadingComment) { + comments.push(leadingComment) + leadingComment = this.parseComment(true) + } + + /** + * check if we have a group choice instead of an assignment + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + if (valuesOrProperties.length === 0) { + throw this.parserError('Unexpected group choice operator "//" at start of group') + } + + if (!isChoice) { + const last = valuesOrProperties.pop() as Property + valuesOrProperties.push([last]) + isChoice = true + } + this.nextToken() + this.nextToken() + continue + } + + const propertyType: PropertyType[] = [] + let isUnwrapped = false + let hasCut = false + let propertyName = '' + + const occurrence = this.parseOccurrences() + + /** + * check if variable name is unwrapped + */ + if (this.curToken.Literal === Tokens.TILDE) { + isUnwrapped = true + this.nextToken() // eat ~ + } + + /** + * parse assignment within array, e.g. + * ``` + * ActionsPerformActionsParameters = [1* { + * type: "key", + * id: text, + * actions: ActionItems, + * *text => any + * }] + * ``` + * or + * ``` + * script.MappingRemoteValue = [*[(script.RemoteValue / text), script.RemoteValue]]; + * ``` + */ + if ( + this.curToken.Literal === Tokens.LBRACE || + this.curToken.Literal === Tokens.LBRACK || + this.curToken.Literal === Tokens.LPAREN + ) { + const innerGroup = this.parseAssignmentValue() as Group + const prop = { + HasCut: false, + Occurrence: occurrence, + Name: '', + Type: innerGroup, + Comments: [] + } + + if (isChoice) { + (valuesOrProperties[valuesOrProperties.length - 1] as Property[]).push(prop) + } else { + valuesOrProperties.push(prop) + } + + if (this.curToken.Type === Tokens.COMMA) { + this.nextToken() + isChoice = false + } + + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) { + if (!isChoice) { + const last = valuesOrProperties.pop() as Property + valuesOrProperties.push([last]) + isChoice = true + } + this.nextToken() + } + continue + } + + /** + * check if we are in an array and a new item is indicated + */ + if (this.curToken.Literal === Tokens.COMMA && closingTokens[0] === Tokens.RBRACK) { + this.nextToken() + continue + } + + propertyName = this.parsePropertyName() + + /** + * an unnamed member decorated with an operator, e.g. `[ bstr .size 3, bstr ]` -- there is no colon here, so this must be handled before the colon-expecting path below ever sees it + */ + if (this.isOperator()) { + const operator = this.parseOperator() + const baseType = PREDEFINED_IDENTIFIER.includes(propertyName) + ? { Type: propertyName as Type } + : { + Type: 'group' as PropertyReferenceType, + Value: propertyName, + Unwrapped: isUnwrapped + } + + valuesOrProperties.push({ + HasCut: hasCut, + Occurrence: occurrence, + Name: '', + Type: [{ ...baseType, Operator: operator }], + Comments: [] + }) + + if (this.curToken.Type === Tokens.COMMA) { + this.nextToken() + } + continue + } + + /** + * if `,` is found we have a group reference and jump to the next line + */ + if (this.curToken.Type === Tokens.COMMA || closingTokens.includes(this.curToken.Type)) { + const tokenType = this.curToken.Type + let parsedComments = false + let comment: Comment | undefined + + /** + * check if line has a comment + */ + if (this.curToken.Type === Tokens.COMMA && this.peekToken.Type === Tokens.COMMENT) { + this.nextToken() + comment = this.parseComment() + parsedComments = true + } + + valuesOrProperties.push({ + HasCut: hasCut, + Occurrence: occurrence, + Name: '', + Type: PREDEFINED_IDENTIFIER.includes(propertyName) + ? propertyName + : [{ + Type: 'group' as PropertyReferenceType, + Value: propertyName, + Unwrapped: isUnwrapped + }], + Comments: comment ? [comment] : [] + }) + + if (this.curToken.Literal === Tokens.COMMA || this.curToken.Literal === closingTokens[0]) { + if (this.curToken.Literal === Tokens.COMMA) { + this.nextToken() + } + continue + } + + if (!parsedComments) { + this.nextToken() + } + + /** + * only continue if next token contains a comma + */ + if (tokenType === Tokens.COMMA) { + continue + } + + /** + * otherwise break + */ + break + } + + /** + * check if property has cut, which happens if a property is described as + * - `? "optional-key" ^ => int,` + * - `? optional-key: int,` - since the colon shortcut includes cuts + */ + if (this.curToken.Type === Tokens.CARET || this.curToken.Type === Tokens.COLON) { + hasCut = true + + if (this.curToken.Type === Tokens.CARET) { + this.nextToken() // eat ^ + } + } + + /** + * check if we have a group choice instead of an assignment + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + const prop: Property = { + HasCut: hasCut, + Occurrence: occurrence, + Name: '', + Type: { + Type: 'group', + Value: propertyName, + Unwrapped: isUnwrapped + }, + Comments: comments + } + + if (isChoice) { + /** + * if we already in a choice just push into it + */ + (valuesOrProperties[valuesOrProperties.length - 1] as Property[]).push(prop) + } else { + /** + * otherwise create a new one + */ + isChoice = true + valuesOrProperties.push([prop]) + } + + this.nextToken() // eat / + this.nextToken() // eat / + continue + } + + /** + * else if no colon was found, throw + */ + if (!this.isPropertyValueSeparator()) { + throw this.parserError('Expected ":" or "=>"') + } + + this.nextToken() // eat : + + /** + * parse property value + */ + const props = this.parseAssignmentValue() + let operator = this.isOperator() ? this.parseOperator() : undefined + if (!isChoice && this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) { + this.nextToken() + const nextType = this.parsePropertyType() + if (Array.isArray(props)) { + /** + * property has not yet been flagged as a choice, but is part + * of one, e.g. `(float .ge 1.0) / null` + */ + props.push(nextType) + if (!this.isOperator()) { + this.nextToken() + } + } + } + if (this.isOperator()) { + operator = this.parseOperator() + } + if (Array.isArray(props)) { + /** + * property has multiple types (e.g. `float / tstr / int`) + */ + propertyType.push(...props) + } else { + propertyType.push(props) + } + + /** + * advance comma + */ + let flipIsChoice = false + if (this.curToken.Type === Tokens.COMMA) { + /** + * if we are in a choice, we leave it here + */ + flipIsChoice = true + + this.nextToken() // eat , + } + + const trailingComment = this.parseComment() + trailingComment && comments.push(trailingComment) + + const prop = { + HasCut: hasCut, + Occurrence: occurrence, + Name: propertyName, + Type: propertyType, + Comments: comments, + ...(operator ? { Operator: operator } : {}) + } + + if (isChoice) { + (valuesOrProperties[valuesOrProperties.length - 1] as Property[]).push(prop) + } else { + valuesOrProperties.push(prop) + } + + if (flipIsChoice) { + isChoice = false + } + + /** + * if `}` is found we are at the end of the group + */ + if (closingTokens.includes(this.curToken.Type)) { + /** + * Handle the case where a group is followed by an inclusion operator, e.g. + * + * group1 = { + * name: tstr, + * age: number, + * } + * + * group2 = { + * handle: tstr + * } .and group1 + * + */ + while (this.peekToken.Type === Tokens.DOT) { + this.nextToken() + if (this.isOperator()) { + valuesOrProperties.push({ + Name: '', + Type: 'group', + Occurrence: DEFAULT_OCCURRENCE, + Operator: this.parseOperator(), + Comments: [], + HasCut: false, + }) + } + } + break + } + + /** + * eat // if we are in a choice + */ + if (isChoice) { + this.nextToken() // eat / + this.nextToken() // eat / + continue + } + } + + /** + * close segment + */ + if (this.curToken.Type === [...closingTokens].shift()) { + this.nextToken() + } + + /** + * if last closing token is "]" we have an array + */ + if (closingTokens[closingTokens.length - 1] === Tokens.RBRACK) { + return { + Type: 'array', + Name: groupName || '', + Values: valuesOrProperties, + Comments: [] + } + } + + /** + * simplify wrapped types, e.g. from + * { + * "Type": "group", + * "Name": "", + * "Properties": [ + * { + * "HasCut": false, + * "Occurrence": { + * "n": 1, + * "m": 1 + * }, + * "Name": "", + * "Type": "bool", + * "Comment": "" + * } + * ], + * "IsChoiceAddition": false + * } + * back to: + * bool + */ + if (!groupName && valuesOrProperties.length === 1 && PREDEFINED_IDENTIFIER.includes((valuesOrProperties[0] as Property).Type as string)) { + return (valuesOrProperties[0] as Property).Type as Assignment + } + + /** + * otherwise a group + */ + return { + Type: 'group', + Name: groupName || '', + Properties: valuesOrProperties, + IsChoiceAddition: isChoiceAddition, + Comments: [] + } + } + + private isPropertyValueSeparator () { + if (this.curToken.Type === Tokens.COLON) { + return true + } + + if (this.curToken.Type === Tokens.ASSIGN && this.peekToken.Type === Tokens.GT) { + this.nextToken() // eat < + return true + } + + return false + } + + /** + * checks if group segment is opened and forwards to beginning of + * first property declaration + * @returns {String[]} closing tokens for group (either `}`, `)` or both) + */ + private openSegment (): string[] { + if (this.curToken.Type === Tokens.LBRACE) { + this.nextToken() + return [Tokens.RBRACE] + } else if (this.curToken.Type === Tokens.LPAREN) { + this.nextToken() + return [Tokens.RPAREN] + } else if (this.curToken.Type === Tokens.LBRACK) { + this.nextToken() + return [Tokens.RBRACK] + } + + return [] + } + + private parsePropertyName (): PropertyName { + /** + * property name without quotes + */ + if (this.curToken.Type === Tokens.IDENT || this.curToken.Type === Tokens.STRING) { + const name = this.curToken.Literal + this.nextToken() + return name + } + + throw this.parserError(`Expected property name, received ${this.curToken.Type}(${this.curToken.Literal}), ${this.peekToken.Type}(${this.peekToken.Literal})`) + } + + private parsePropertyType (): PropertyType { + let type: PropertyType | undefined = undefined + let isUnwrapped = false + let isGroupedRange = false + + /** + * check if variable name is unwrapped + */ + if (this.curToken.Literal === Tokens.TILDE) { + isUnwrapped = true + this.nextToken() // eat ~ + } + + /** + * a quoted string is always a literal, even if its text matches a + * reserved keyword like "null" or "bool" + */ + switch (this.curToken.Type === Tokens.STRING ? Tokens.STRING : this.curToken.Literal) { + case Type.ANY: + case Type.BOOL: + case Type.INT: + case Type.UINT: + case Type.NINT: + case Type.FLOAT: + case Type.FLOAT16: + case Type.FLOAT32: + case Type.FLOAT64: + case Type.BSTR: + case Type.BYTES: + case Type.TSTR: + case Type.TEXT: + case Type.NIL: + case Type.NULL: + type = this.curToken.Literal + break + default: { + if (this.curToken.Type === Tokens.STRING) { + type = { + Type: 'literal' as PropertyReferenceType, + Value: this.curToken.Literal, + Unwrapped: isUnwrapped + } + } else if (BOOLEAN_LITERALS.includes(this.curToken.Literal)) { + type = { + Type: 'literal' as PropertyReferenceType, + Value: this.curToken.Literal === 'true', + Unwrapped: isUnwrapped + } + } else if (this.curToken.Literal === Tokens.LBRACE || this.curToken.Literal === Tokens.LBRACK) { + const val = this.parseAssignmentValue() + if (Array.isArray(val)) { + throw new Error('Unexpected array in property type parsing') + } + type = val + } else if (this.curToken.Type === Tokens.IDENT) { + type = { + Type: 'group' as PropertyReferenceType, + Value: this.curToken.Literal, + Unwrapped: isUnwrapped + } + } else if (this.curToken.Type === Tokens.NUMBER || this.curToken.Type === Tokens.FLOAT) { + type = { + Type: 'literal' as PropertyReferenceType, + Value: parseNumberValue(this.curToken), + Unwrapped: isUnwrapped, + ...(this.curToken.Type === Tokens.FLOAT ? { IsFloat: true } : {}) + } + } else if (this.curToken.Type === Tokens.HASH) { + this.nextToken() + const n = parseNumberValue(this.curToken) + this.nextToken() // eat numeric value + this.nextToken() // eat ( + const t = this.parsePropertyType() + this.nextToken() // eat ) + type = { + Type: 'tag', + Value: { + NumericPart: n as number, + TypePart: t as string + }, + Unwrapped: isUnwrapped + } + } else if (this.curToken.Literal === Tokens.LPAREN && this.peekToken.Type === Tokens.NUMBER) { + this.nextToken() + type = { + Type: 'literal' as PropertyReferenceType, + Value: parseNumberValue(this.curToken), + Unwrapped: isUnwrapped + } + isGroupedRange = true + } else { + throw this.parserError(`Invalid property type "${this.curToken.Literal}"`) + } + } + } + + /** + * check if type continue as a range + */ + if ( + this.peekToken.Type === Tokens.DOT && + this.nextToken() && + this.peekToken.Type === Tokens.DOT + ) { + this.nextToken() + let Inclusive = true + + /** + * check if range excludes upper bound + */ + if (this.peekToken.Type === Tokens.DOT) { + Inclusive = false + this.nextToken() + } + + this.nextToken() + + if (!type || (typeof type === 'object' && !('Value' in type))) { + throw new Error('Invalid type for range definition') + } + + const Min: RangePropertyReference = typeof type === 'string' || typeof type.Value === 'number' + ? type as string + : type.Value as (number | string) + type = { + Type: 'range' as PropertyReferenceType, + Value: { + Inclusive, + Min, + Max: this.parsePropertyType() as RangePropertyReference + }, + Unwrapped: isUnwrapped + } + + if (!isGroupedRange && this.peekToken.Literal === Tokens.RPAREN) { + /** + * If we are at the end of a grouped range, and this was called + * on the first item of the range as opposed to the opening + * parenthesis, isGroupedRange will not be set to true at this + * point. We need to advance to the closing parenthesis, and if + * the next token is an operator, we need to advance to the dot + * so that parseOperator will work properly. + * e.g. + * + * ``` + * (1.0..2.0) .default 1.5 + * ``` + * + * This will be called on the `1.0` and then the `2.0` will be parsed + * as a grouped range. + */ + this.nextToken() + if (this.isOperator()) { + isGroupedRange = true + } + } + + if (isGroupedRange) { + this.nextToken() // eat ")" + } + } + + if (!type) { + const { line, position: column } = this.l.getLocation() + throw new Error(`Unexpected type: ${this.curToken.Type} at line ${line} column ${column}`) + } + + return type + } + + private parseOperator (): Operator { + const type = this.peekToken.Literal as OperatorType + if (this.curToken.Literal !== Tokens.DOT || !OPERATORS.includes(this.peekToken.Literal as OperatorType)) { + throw new Error(`Operator ".${type}", expects a ${OPERATORS_EXPECTING_VALUES[type]!.join(' or ')} property, but found ${this.peekToken.Literal}!`) + } + + this.nextToken() // eat "." + this.nextToken() // eat operator type + const value = this.parsePropertyType() as PropertyReferenceType + this.nextToken() // eat operator value + return { + Type: type, + Value: value + } + } + + private isOperator () { + return this.curToken.Literal === Tokens.DOT && OPERATORS.includes(this.peekToken.Literal as OperatorType) + } + + private parsePropertyTypes (attachChoiceOperators = false): PropertyType[] { + const propertyTypes: PropertyType[] = [] + + let prop: PropertyType = this.parsePropertyType() + if (this.isOperator()) { + prop = { + Type: prop, + Operator: this.parseOperator() + } as NativeTypeWithOperator + } else if (this.curToken.Type !== Tokens.SLASH) { + this.nextToken() // eat Property if not already consumed (e.g. by Group parsing) + } + + propertyTypes.push(prop) + + /** + * ignore comments between type choice members, e.g. + * ``` + * Foo = int ; comment + * / text + * ``` + * or + * ``` + * Foo = int / ; comment + * text + * ``` + */ + while (this.curToken.Type === Tokens.COMMENT && this.peekToken.Type === Tokens.SLASH) { + this.parseComment() + } + + /** + * ensure we don't go into the next choice, e.g.: + * ``` + * delivery = ( + * city // lala: tstr / bool // per-pickup: true, + * ) + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + return propertyTypes + } + + /** + * capture more if available (e.g. `tstr / float / boolean`) + */ + while (this.curToken.Type === Tokens.SLASH) { + this.nextToken() // eat `/` + while ([Tokens.COMMENT].includes(this.curToken.Type)) { + this.parseComment() + } + let nextProp: PropertyType = this.parsePropertyType() + if (this.isOperator()) { + if (attachChoiceOperators) { + nextProp = { + Type: nextProp, + Operator: this.parseOperator() + } as NativeTypeWithOperator + } + } else if (this.curToken.Type !== Tokens.SLASH) { + /** + * If we are not parsing an operator, we need to eat the next token; + * otherwise, the operator will be parsed by the caller + */ + this.nextToken() + } + propertyTypes.push(nextProp) + + while ([Tokens.COMMENT].includes(this.curToken.Type) && this.peekToken.Type === Tokens.SLASH) { + this.parseComment() + } + + /** + * ensure we don't go into the next choice, e.g.: + * ``` + * delivery = ( + * city // lala: tstr / bool // per-pickup: true, + * ) + */ + if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) { + break + } + } + + return propertyTypes + } + + private parseOccurrences () { + let occurrence = DEFAULT_OCCURRENCE + + /** + * check for non-numbered occurrence indicator, e.g. zero or more: + * ``` + * * bedroom: size, + * ``` + * zero or one: + * ``` + * ? bedroom: size, + * ``` + * or one or more: + * ``` + * + bedroom: size, + * ``` + */ + if (this.curToken.Type === Tokens.QUEST || this.curToken.Type === Tokens.ASTERISK || this.curToken.Type === Tokens.PLUS) { + const n = this.curToken.Type === Tokens.PLUS ? 1 : 0 + let m = this.curToken.Type === Tokens.QUEST ? 1 : Infinity + + /** + * check if there is a max definition + */ + if (this.peekToken.Type === Tokens.NUMBER) { + m = parseInt(this.peekToken.Literal, 10) + this.nextToken() + } + + occurrence = { n, m } + this.nextToken() + /** + * numbered occurrence indicator, e.g. + * ``` + * 1*10 bedroom: size, + * ``` + */ + } else if ( + this.curToken.Type === Tokens.NUMBER && + this.peekToken.Type === Tokens.ASTERISK + ) { + const n = parseInt(this.curToken.Literal, 10) + let m = Infinity + this.nextToken() // eat "n" + this.nextToken() // eat "*" + + /** + * check if there is a max definition + */ + if (this.curToken.Type === Tokens.NUMBER) { + m = parseInt(this.curToken.Literal, 10) + this.nextToken() + } + + occurrence = { n, m } + } + + return occurrence + } + + /** + * check if line has a comment + */ + private parseComment (isLeading?: boolean): Comment | undefined { + if (this.curToken.Type !== Tokens.COMMENT) { + return + } + const comment = this.curToken.Literal.replace(/^;(\s*)/, '') + this.nextToken() + + if (comment.trim().length === 0) { + return + } + + return { Type: 'comment', Content: comment, Leading: Boolean(isLeading) } + } + + parse () { + const definition: Assignment[] = [] + + while (this.curToken.Type !== Tokens.EOF) { + const group = this.parseAssignments() + if (group) { + definition.push(group) + } + } + + return definition + } + + private parserError (message: string) { + const location = this.l.getLocation() + const locInfo = this.l.getLocationInfo() + return new Error(`${this.#filePath.replace(process.cwd(), '')}:${location.line + 1}:${location.position} - error: ${message}\n\n${locInfo}`) + } +} diff --git a/vendor/cddl/src/tokens.ts b/vendor/cddl/src/tokens.ts new file mode 100644 index 0000000..b7a81da --- /dev/null +++ b/vendor/cddl/src/tokens.ts @@ -0,0 +1,50 @@ +export type TokenType = string + +export type Token = { + Type: TokenType + Literal: string +} + +export enum Tokens { + ILLEGAL = 'ILLEGAL', + EOF = 'EOF', + NL = '\n', + SPACE = ' ', + UNDERSCORE = '_', + DOLLAR = '$', + ATSIGN = '@', + CARET = '^', + HASH = '#', + TILDE = '~', + + // Identifiers + literals, + IDENT = 'IDENT', + INT = 'INT', + COMMENT = 'COMMENT', + STRING = 'STRING', + NUMBER = 'NUMBER', + FLOAT = 'FLOAT', + + // Operators, + ASSIGN = '=', + PLUS = '+', + MINUS = '-', + SLASH = '/', + QUEST = '?', + ASTERISK = '*', + + // Delimiters, + COMMA = ',', + DOT = '.', + COLON = ':', + SEMICOLON = ';', + LPAREN = '(', + RPAREN = ')', + LBRACE = '{', + RBRACE = '}', + LBRACK = '[', + RBRACK = ']', + LT = '<', + GT = '>', + QUOT = '"' +} diff --git a/vendor/cddl/src/utils.ts b/vendor/cddl/src/utils.ts new file mode 100644 index 0000000..6e1a4c9 --- /dev/null +++ b/vendor/cddl/src/utils.ts @@ -0,0 +1,121 @@ +import camelcase from 'camelcase' + +import type { + Assignment, + Array as CDDLArray, + Group, + NativeTypeWithOperator, + Property, + PropertyReference, + Variable +} from './ast.js' + +import { Tokens, Token } from './tokens.js' + +export function isLetter (ch: string): boolean { + return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' +} + +export function isAlphabeticCharacter (ch: string): boolean { + return isLetter(ch) || ch === Tokens.ATSIGN || ch === Tokens.UNDERSCORE || ch === Tokens.DOLLAR +} + +export function isDigit (ch: string): boolean { + return !isNaN(ch as unknown as number) && ch !== Tokens.NL && ch !== Tokens.SPACE +} + +export function hasSpecialNumberCharacter (ch: number) { + return ( + ch === Tokens.MINUS.charCodeAt(0) || + ch === Tokens.DOT.charCodeAt(0) || + ch === 'x'.charCodeAt(0) || + ch === 'b'.charCodeAt(0) + ) +} + +export function parseNumberValue (token: Token): string | number { + if (token.Type === Tokens.FLOAT) { + return parseFloat(token.Literal) + } + + if ( + token.Literal.includes('x') || + token.Literal.includes('b') + ) { + return token.Literal + } + + return parseInt(token.Literal, 10) +} + +export function pascalCase (name: string) { + return camelcase(name, { pascalCase: true }) +} + +export function isVariable (assignment: Assignment): assignment is Variable { + return assignment.Type === 'variable' +} + +export function isGroup (t: any): t is Group { + return t && t.Type === 'group' +} + +export function isCDDLArray (t: any): t is CDDLArray { + return t && t.Type === 'array' +} + +export function isProperty (t: any): t is Property { + return t && typeof t.Name === 'string' && typeof t.HasCut === 'boolean' +} + +export function isUnNamedProperty (t: any): t is Property & { Name: '' } { + return isProperty(t) && t.Name === '' +} + +export function isNamedGroupReference (t: any): t is PropertyReference & { Value: string } { + return isGroup(t) && isPropertyReference(t) && typeof t.Value === 'string' +} + +export function isPropertyReference (t: any): t is PropertyReference { + return t && typeof t === 'object' && 'Value' in t +} + +export function isNativeTypeWithOperator (t: any): t is NativeTypeWithOperator { + return Boolean( + t && + typeof t === 'object' && + 'Type' in t && + !('Value' in t) && + t.Operator && + typeof t.Operator === 'object' + ) +} + +export function getRegexpPattern (t: any): string | undefined { + if (!isNativeTypeWithOperator(t)) { + return + } + + if (typeof t.Type !== 'string' || !['str', 'text', 'tstr'].includes(t.Type)) { + return + } + + if (t.Operator?.Type !== 'regexp' || !isLiteralWithValue(t.Operator.Value)) { + return + } + + return typeof t.Operator.Value.Value === 'string' + ? t.Operator.Value.Value + : undefined +} + +export function isRange (t: any): boolean { + return t && typeof t.Type === 'object' && (t.Type as any).Type === 'range' +} + +export function isLiteralWithValue (t: any): t is { + Type: 'literal' + Value: unknown +} { + return t && t.Type === 'literal' && 'Value' in t +} diff --git a/vendor/cddl/tsconfig.json b/vendor/cddl/tsconfig.json new file mode 100644 index 0000000..d6b3b7d --- /dev/null +++ b/vendor/cddl/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +}