diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c2ca659 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Force LF in the working tree on every platform. +# +# Windows checks out CRLF by default (core.autocrlf=true). server.mjs starts +# with a `#!/usr/bin/env node` shebang, and Vite's SSR transform — which vitest +# runs the module through — fails to parse that shebang when the line ends in +# CRLF: the transform hoists the imports above it and then chokes on +# "Invalid Character `!`", so the whole suite dies with a SyntaxError before a +# single test runs. Pinning LF keeps the suite behaving identically everywhere. +* text=auto eol=lf + +# Shell scripts must keep LF to stay executable on POSIX. +*.sh text eol=lf + +*.tgz binary diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df86e8e..f0f7c66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,12 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: + os: [ubuntu-latest, windows-latest] node-version: [20, 22, 24] steps: @@ -26,8 +28,12 @@ jobs: - name: Install dependencies run: npm ci + # Coverage thresholds are pinned at 100% in vitest.config.mjs, so this + # also fails the build if a change adds an untested line or branch. - name: Run tests run: npx vitest run --coverage + # The dependency tree is platform independent — auditing it once is enough. - name: Security audit + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 22 run: npm audit --audit-level=high diff --git a/.npmignore b/.npmignore index 981867d..2b07da4 100644 --- a/.npmignore +++ b/.npmignore @@ -21,6 +21,7 @@ node_modules/ # Tests test-ssh/ *.test.mjs +vitest.config.mjs coverage/ # Generated files diff --git a/CHANGELOG.md b/CHANGELOG.md index be545d3..d037193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Security +- Resolved all 17 open `npm audit` advisories (12 high, 4 moderate, 1 low) that accumulated since the 1.3.7 cleanup. `npm audit` now reports zero vulnerabilities again. `npm audit fix` could not be used — it aborts with an internal npm error (`Cannot read properties of null (reading 'edgesOut')`) on this tree's `overrides` — so the fixes are pinned explicitly: + - Direct bumps, all within the existing semver range: `@modelcontextprotocol/sdk` 1.27.1 → 1.30.0, `vitest`/`@vitest/coverage-v8` 4.1.4 → 4.1.10, `@anthropic-ai/dxt` 0.2.5 → 0.2.6. These cleared the `vite`, `postcss` and `nanoid` advisories. + - Raised the existing `tmp` override from `>=0.2.4` to `>=0.2.6` (GHSA path traversal via unsanitized prefix/postfix), which cleared the whole `@anthropic-ai/dxt → @inquirer/prompts → @inquirer/editor → external-editor → tmp` chain. + - New overrides for the transitive HTTP-stack advisories reachable through `@modelcontextprotocol/sdk`: `brace-expansion`, `fast-uri`, `ip-address`, `hono`, `@hono/node-server`, `body-parser`, `qs`, `express-rate-limit`. Each is pinned to the lowest version that carries the fix. As in 1.3.7, none of this code is reachable from this package: it belongs to the SDK's HTTP/SSE transport, and mcp-ssh only ever loads `server/stdio.js`. +- Verified after the bumps: full suite green, `npm ci` reproducible from the lockfile, and the real server answers `initialize` and `tools/list` correctly over STDIO. + +### Fixed +- **Multi-alias hosts (fixes #12)**: A host declared under several aliases (`Host docker-lxc hlab`) was unreachable under *any* of its names. `ssh-config@5` returns a plain string for a single-token value but an array of token objects (`{val, separator, quoted}`) once a directive carries more than one token; `extractHostsFromConfig` stored that array in `alias` verbatim, so every strict comparison downstream (`_assertKnownHostAlias`, `getHostInfo`, `getPasswordForHost`, `getAllKnownHosts`) compared a string against an array and never matched. The host was listed by `listKnownHosts` but rejected by the known-host gate before `ssh` was ever spawned. ssh-config values are now normalized once at parse time: `alias` keeps the first alias (output shape unchanged), a new `aliases` field carries the full list, and matching goes through a shared `hostMatchesAlias()` helper. Contributed by @badigit. +- **Wildcard blocks with negations**: `Host * !bastion` was emitted as a connectable host if it carried a `HostName`. The old `section.value !== '*'` check could not match a multi-token value, which is an array. Blocks consisting only of wildcards and negated patterns are now skipped as the defaults blocks they are. +- **Multi-token directives**: `ProxyCommand`, `SendEnv`, `IPQoS` and friends were surfaced in `listKnownHosts` output as arrays of token objects instead of readable strings. They are now flattened. + +### Changed +- **Windows test suite**: 14 tests silently asserted POSIX-only behaviour (the `chmod 600` config check, the `/bin/sh` askpass helper, `detached`, a bare `ssh` as argv[0]) and failed when the suite ran on Windows. Both platform paths are now asserted explicitly by re-importing the module with `process.platform` faked, so the suite is meaningful and green on either OS. `SSH_BIN`/`SCP_BIN` are exported so tests assert against the binary the module actually resolved. +- **CI**: the test matrix now runs on `windows-latest` in addition to `ubuntu-latest`, across Node 20/22/24. +- **Coverage**: `server.mjs` is at 100% statements, branches, functions and lines, and `vitest.config.mjs` pins those thresholds so a change adding an untested line or branch fails the build. +- Dropped the `process.env.Path` fallback in `resolveExecutable()`: Node exposes `process.env` case-insensitively on Windows, so `process.env.PATH` already resolves a variable spelled `Path`. The fallback was unreachable. + ## [1.3.8] - 2026-04-14 ### Fixed diff --git a/package-lock.json b/package-lock.json index 4a775d4..9614f87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.3.8", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", + "@modelcontextprotocol/sdk": "^1.30.0", "glob": "^11.0.3", "ssh-config": "^5.0.0" }, @@ -17,23 +17,24 @@ "mcp-ssh": "bin/mcp-ssh.js" }, "devDependencies": { - "@anthropic-ai/dxt": "^0.2.5", + "@anthropic-ai/dxt": "^0.2.6", "@types/node": "^20.11.26", "@types/ssh2": "^1.15.0", - "@vitest/coverage-v8": "^4.1.4", - "tmp": ">=0.2.4", + "@vitest/coverage-v8": "^4.1.10", + "tmp": ">=0.2.6", "ts-node": "^10.9.2", "typescript": "^5.4.3", - "vitest": "^4.1.4" + "vitest": "^4.1.10" }, "engines": { "node": ">=20" } }, "node_modules/@anthropic-ai/dxt": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@anthropic-ai/dxt/-/dxt-0.2.5.tgz", - "integrity": "sha512-vFibMY6+8dlT0cqZIjN9TES7x/V+sQ3SUGKCgqxb1TkpXELW2IhYkNB975FKV4mrCOlc15M6QQoiKwkrJIHTRA==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@anthropic-ai/dxt/-/dxt-0.2.6.tgz", + "integrity": "sha512-5VSqKRpkytTYh5UJz9jOaI8zLXNCe4Gc+ArKGFV6IeWnEPP0Qnd0k+V3pO8cYzp92Puf/+Cgo0xc4haE0azTXg==", + "deprecated": "This package has been renamed to @anthropic-ai/mcpb", "dev": true, "license": "MIT", "dependencies": { @@ -123,47 +124,13 @@ "node": ">=12" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@hono/node-server": { - "version": "1.19.13", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", - "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", + "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -532,12 +499,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -571,29 +538,10 @@ } } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", "funding": { @@ -601,9 +549,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", "cpu": [ "arm64" ], @@ -618,9 +566,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", "cpu": [ "arm64" ], @@ -635,9 +583,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", "cpu": [ "x64" ], @@ -652,9 +600,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", "cpu": [ "x64" ], @@ -669,9 +617,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", "cpu": [ "arm" ], @@ -686,9 +634,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", "cpu": [ "arm64" ], @@ -703,9 +651,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", "cpu": [ "arm64" ], @@ -720,9 +668,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", "cpu": [ "ppc64" ], @@ -737,9 +685,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", "cpu": [ "s390x" ], @@ -754,9 +702,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", "cpu": [ "x64" ], @@ -771,9 +719,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", "cpu": [ "x64" ], @@ -788,9 +736,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", "cpu": [ "arm64" ], @@ -804,29 +752,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", "cpu": [ "arm64" ], @@ -841,9 +770,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", "cpu": [ "x64" ], @@ -858,9 +787,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -899,17 +828,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -990,14 +908,14 @@ "license": "MIT" }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", - "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.10", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -1011,8 +929,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.4", - "vitest": "4.1.4" + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1021,16 +939,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", - "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1039,9 +957,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", - "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1052,13 +970,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", - "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -1066,14 +984,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", - "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1082,9 +1000,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", - "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -1092,13 +1010,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", - "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1268,20 +1186,20 @@ } }, "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1291,16 +1209,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/bytes": { @@ -1583,16 +1514,16 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1648,9 +1579,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1701,12 +1632,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "debug": "^4.4.3", + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -1753,9 +1685,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz", + "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==", "funding": [ { "type": "github", @@ -2019,9 +1951,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2031,9 +1963,9 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -2099,9 +2031,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "license": "MIT", "engines": { "node": ">= 12" @@ -2233,9 +2165,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -2249,23 +2181,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "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" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -2284,9 +2216,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -2305,9 +2237,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -2326,9 +2258,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -2347,9 +2279,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -2368,9 +2300,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -2389,9 +2321,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -2410,9 +2342,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -2431,9 +2363,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -2452,9 +2384,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -2473,9 +2405,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -2643,9 +2575,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2702,15 +2634,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/on-finished": { "version": "2.4.1", @@ -2798,9 +2733,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -2820,9 +2755,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -2840,7 +2775,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2875,12 +2810,13 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -2923,14 +2859,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2939,21 +2875,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, "node_modules/router": { @@ -3064,14 +2999,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -3083,13 +3018,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -3187,9 +3122,9 @@ } }, "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -3268,9 +3203,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -3278,9 +3213,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3295,9 +3230,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -3305,9 +3240,9 @@ } }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -3367,14 +3302,6 @@ } } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -3389,17 +3316,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -3459,19 +3403,19 @@ } }, "node_modules/vitest": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", - "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.4", - "@vitest/mocker": "4.1.4", - "@vitest/pretty-format": "4.1.4", - "@vitest/runner": "4.1.4", - "@vitest/snapshot": "4.1.4", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -3499,12 +3443,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.4", - "@vitest/browser-preview": "4.1.4", - "@vitest/browser-webdriverio": "4.1.4", - "@vitest/coverage-istanbul": "4.1.4", - "@vitest/coverage-v8": "4.1.4", - "@vitest/ui": "4.1.4", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -3549,13 +3493,13 @@ } }, "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", - "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.4", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3576,17 +3520,17 @@ } }, "node_modules/vitest/node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -3602,7 +3546,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/package.json b/package.json index a1e125e..1e00723 100644 --- a/package.json +++ b/package.json @@ -45,21 +45,29 @@ "access": "public" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", + "@modelcontextprotocol/sdk": "^1.30.0", "glob": "^11.0.3", "ssh-config": "^5.0.0" }, "devDependencies": { - "@anthropic-ai/dxt": "^0.2.5", + "@anthropic-ai/dxt": "^0.2.6", "@types/node": "^20.11.26", "@types/ssh2": "^1.15.0", - "@vitest/coverage-v8": "^4.1.4", - "tmp": ">=0.2.4", + "@vitest/coverage-v8": "^4.1.10", + "tmp": ">=0.2.6", "ts-node": "^10.9.2", "typescript": "^5.4.3", - "vitest": "^4.1.4" + "vitest": "^4.1.10" }, "overrides": { - "tmp": ">=0.2.4" + "tmp": ">=0.2.6", + "brace-expansion": ">=5.0.9", + "fast-uri": ">=3.1.5", + "ip-address": ">=10.5.0", + "hono": ">=4.13.1", + "@hono/node-server": ">=2.0.5", + "body-parser": ">=2.3.0", + "qs": ">=6.15.2", + "express-rate-limit": ">=8.5.1" } } diff --git a/server.mjs b/server.mjs index 976977f..5a07c59 100755 --- a/server.mjs +++ b/server.mjs @@ -35,7 +35,9 @@ const isWindows = process.platform === 'win32'; // already searches PATH safely). function resolveExecutable(name) { if (!isWindows) return name; - const pathDirs = (process.env.PATH || process.env.Path || '').split(';'); + // No `|| process.env.Path` fallback: Node exposes process.env case-insensitively + // on Windows, so process.env.PATH already resolves a variable spelled `Path`. + const pathDirs = (process.env.PATH || '').split(';'); const exts = (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';'); for (const dir of pathDirs) { if (!dir) continue; @@ -67,6 +69,40 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); +// --------------------------------------------------------------------------- +// ssh-config@5 value normalization. +// +// The parser returns a plain string for a single-token value, but an array of +// token objects ({ val, separator, quoted }) as soon as a multi-value directive +// carries more than one token. Affected directives (ssh-config/lib/ssh-config.js): +// Host, Match, ProxyCommand, SendEnv, IPQoS, CanonicalDomains, +// GlobalKnownHostsFile, UserKnownHostsFile +// Everything downstream must go through these helpers, otherwise a multi-alias +// `Host a b` block is stored with an array where a string is expected and no +// strict comparison against it can ever match. +// --------------------------------------------------------------------------- +function configValueTokens(value) { + if (value == null) return []; + if (Array.isArray(value)) { + return value + .map(v => (v && typeof v === 'object' && 'val' in v ? v.val : String(v))) + .filter(v => v !== ''); + } + return [String(value)]; +} + +function configValueToString(value) { + return configValueTokens(value).join(' '); +} + +// True if `alias` names this host — via any of its aliases or its hostname. +function hostMatchesAlias(host, alias) { + if (!host || !alias) return false; + if (host.hostname === alias) return true; + if (Array.isArray(host.aliases)) return host.aliases.includes(alias); + return host.alias === alias; +} + // SSH Configuration Parser class SSHConfigParser { constructor() { @@ -171,10 +207,20 @@ class SSHConfigParser { continue; } - if (section.param === 'Host' && section.value !== '*') { + if (section.param === 'Host') { + const aliases = configValueTokens(section.value); + + // Skip blocks that only carry defaults (`Host *`, `Host * !bastion`): + // they are not connectable hosts. The old `section.value !== '*'` check + // missed these because a multi-token Host value is an array, never '*'. + if (aliases.length === 0 || aliases.every(a => a === '*' || a.startsWith('!'))) { + continue; + } + const hostInfo = { hostname: '', - alias: section.value, + alias: aliases[0], // first alias — keeps the existing output shape + aliases, // full list — used for matching configFile: configPath }; @@ -195,22 +241,26 @@ class SSHConfigParser { continue; } + // Multi-token directives (ProxyCommand, SendEnv, IPQoS, …) arrive as + // arrays of token objects; flatten so the JSON we hand back is readable. + const value = configValueToString(param.value); + switch (param.param.toLowerCase()) { case 'hostname': - hostInfo.hostname = param.value; + hostInfo.hostname = value; break; case 'user': - hostInfo.user = param.value; + hostInfo.user = value; break; case 'port': - hostInfo.port = parseInt(param.value, 10); + hostInfo.port = parseInt(value, 10); break; case 'identityfile': - hostInfo.identityFile = param.value; + hostInfo.identityFile = value; break; default: // Store other parameters - hostInfo[param.param.toLowerCase()] = param.value; + hostInfo[param.param.toLowerCase()] = value; } } @@ -269,9 +319,7 @@ class SSHConfigParser { // Add hosts from known_hosts that aren't already in the config // These will appear after the config hosts for (const hostname of knownHostnames) { - if (!configHosts.some(host => - host.hostname === hostname || - host.alias === hostname)) { + if (!configHosts.some(host => hostMatchesAlias(host, hostname))) { allHosts.push({ hostname: hostname, source: 'known_hosts' @@ -323,10 +371,8 @@ class SSHClient { const cleanAlias = hostAlias.includes('@') ? hostAlias.split('@').pop() : hostAlias; const knownHosts = await this.configParser.getAllKnownHosts(); const isKnown = knownHosts.some((host) => - host.alias === hostAlias || - host.hostname === hostAlias || - host.alias === cleanAlias || - host.hostname === cleanAlias + hostMatchesAlias(host, hostAlias) || + hostMatchesAlias(host, cleanAlias) ); if (!isKnown) { throw new Error(`Unknown hostAlias: ${hostAlias} is not defined in ~/.ssh/config or ~/.ssh/known_hosts`); @@ -337,7 +383,7 @@ class SSHClient { // Strip user@ prefix if present (e.g. "test@ssh-test" -> "ssh-test") const cleanAlias = hostAlias.includes('@') ? hostAlias.split('@').pop() : hostAlias; const hosts = await this.configParser.processIncludeDirectives(this.configParser.configPath); - const host = hosts.find(h => h.alias === cleanAlias || h.hostname === cleanAlias); + const host = hosts.find(h => hostMatchesAlias(h, cleanAlias)); return host?._password || null; } @@ -470,7 +516,7 @@ class SSHClient { async getHostInfo(hostAlias) { const hosts = await this.configParser.processIncludeDirectives(this.configParser.configPath); - const host = hosts.find(host => host.alias === hostAlias || host.hostname === hostAlias) || null; + const host = hosts.find(host => hostMatchesAlias(host, hostAlias)) || null; if (host) { // Never expose password to the LLM const { _password, ...safeHost } = host; @@ -830,4 +876,7 @@ async function main() { // caused the server to silently exit when launched via bin/mcp-ssh.js on // Windows MCP clients (issue #8). The bin wrapper now imports and calls // main() explicitly. -export { SSHConfigParser, SSHClient, debugLog, main }; +// SSH_BIN/SCP_BIN are exported so tests can assert against the binary this +// module actually resolved instead of hardcoding 'ssh'/'scp' — on Windows they +// are absolute paths (see resolveExecutable). +export { SSHConfigParser, SSHClient, debugLog, main, SSH_BIN, SCP_BIN }; diff --git a/server.test.mjs b/server.test.mjs index b50eca9..3ca55e2 100644 --- a/server.test.mjs +++ b/server.test.mjs @@ -1,6 +1,9 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'; import { createRequire } from 'module'; import { EventEmitter } from 'events'; +import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; const require = createRequire(import.meta.url); const sshConfigLib = require('ssh-config'); @@ -19,7 +22,44 @@ vi.mock('fs/promises', async () => { }); import { readFile, stat, writeFile, chmod } from 'fs/promises'; -import { SSHConfigParser, SSHClient, main } from './server.mjs'; +import { SSHConfigParser, SSHClient, main, SSH_BIN, SCP_BIN } from './server.mjs'; + +// Load a fresh copy of server.mjs with process.platform (and optionally parts of +// the environment) faked, so both the POSIX and the Windows branches can be +// exercised from any host OS. The module snapshots `isWindows`, SSH_BIN and +// SCP_BIN at load time, so the platform only has to stay patched across the +// import itself — hence the restore in `finally`. +// +// Without this, ~14 tests silently assert POSIX-only behaviour (chmod 600 +// checks, the /bin/sh askpass helper, `detached`, a bare 'ssh' argv[0]) and fail +// when the suite runs on Windows. +async function loadServerAs(platform, envOverrides = {}) { + const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + const realEnv = {}; + + for (const [key, value] of Object.entries(envOverrides)) { + realEnv[key] = process.env[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + vi.resetModules(); + + try { + const server = await import('./server.mjs'); + // fs/promises has to be re-imported from the same fresh module graph: + // resetModules re-runs the vi.mock factory, so these are new spies — not the + // ones bound by the static import above. + const fs = await import('fs/promises'); + return { ...server, fs }; + } finally { + Object.defineProperty(process, 'platform', realPlatform); + for (const key of Object.keys(envOverrides)) { + if (realEnv[key] === undefined) delete process.env[key]; + else process.env[key] = realEnv[key]; + } + } +} // Helper: create a fake spawn that returns a mock child process function createMockSpawn({ stdout = '', stderr = '', code = 0, error = null } = {}) { @@ -154,6 +194,60 @@ Host myhost expect(hosts[0].alias).toBe('myhost'); }); + // Regression: ssh-config@5 returns a plain string for a single-token value + // but an array of token objects for `Host a b`. Storing that array in + // `alias` made every strict comparison fail, so a multi-alias host was + // unreachable under *any* of its names. + it('should expose every alias of a multi-alias Host block', () => { + const config = sshConfigLib.parse(` +Host docker-lxc hlab + HostName 10.9.0.105 + User root +`); + const hosts = parser.extractHostsFromConfig(config, '/test'); + + expect(hosts).toHaveLength(1); + expect(hosts[0].aliases).toEqual(['docker-lxc', 'hlab']); + expect(hosts[0].alias).toBe('docker-lxc'); + expect(hosts[0].hostname).toBe('10.9.0.105'); + }); + + it('should keep alias a string for single-alias hosts', () => { + const config = sshConfigLib.parse(` +Host solo + HostName 1.2.3.4 +`); + const hosts = parser.extractHostsFromConfig(config, '/test'); + + expect(hosts[0].alias).toBe('solo'); + expect(hosts[0].aliases).toEqual(['solo']); + }); + + it('should skip a wildcard block carrying negations', () => { + const config = sshConfigLib.parse(` +Host * !bastion + HostName 7.7.7.7 + +Host myhost + HostName 1.2.3.4 +`); + const hosts = parser.extractHostsFromConfig(config, '/test'); + + expect(hosts).toHaveLength(1); + expect(hosts[0].alias).toBe('myhost'); + }); + + it('should flatten multi-token directives into a string', () => { + const config = sshConfigLib.parse(` +Host jump + HostName localhost + ProxyCommand ssh bastion -W %h:%p +`); + const hosts = parser.extractHostsFromConfig(config, '/test'); + + expect(hosts[0].proxycommand).toBe('ssh bastion -W %h:%p'); + }); + it('should skip Include directives', () => { const config = sshConfigLib.parse(SAMPLE_SSH_CONFIG_WITH_INCLUDE); const hosts = parser.extractHostsFromConfig(config, '/test'); @@ -252,37 +346,60 @@ Host test }); }); - describe('checkFilePermissions', () => { + // Unix permission bits have no meaning on Windows, so checkFilePermissions is + // a deliberate no-op there. Pin the platform instead of inheriting the host's, + // otherwise every expectation below is wrong on one OS or the other. + describe('checkFilePermissions (POSIX)', () => { + let posixParser; + let posixStat; + + beforeEach(async () => { + const posix = await loadServerAs('linux'); + posixParser = new posix.SSHConfigParser(); + posixStat = posix.fs.stat; + }); + it('should pass with 600 permissions', async () => { - stat.mockResolvedValue({ mode: 0o100600 }); - await expect(parser.checkFilePermissions('/test')).resolves.not.toThrow(); + posixStat.mockResolvedValue({ mode: 0o100600 }); + await expect(posixParser.checkFilePermissions('/test')).resolves.not.toThrow(); }); it('should throw on insecure permissions (644)', async () => { - stat.mockResolvedValue({ mode: 0o100644 }); - await expect(parser.checkFilePermissions('/test')).rejects.toThrow('insecure permissions'); + posixStat.mockResolvedValue({ mode: 0o100644 }); + await expect(posixParser.checkFilePermissions('/test')).rejects.toThrow('insecure permissions'); }); it('should throw on insecure permissions (755)', async () => { - stat.mockResolvedValue({ mode: 0o100755 }); - await expect(parser.checkFilePermissions('/test')).rejects.toThrow('insecure permissions'); + posixStat.mockResolvedValue({ mode: 0o100755 }); + await expect(posixParser.checkFilePermissions('/test')).rejects.toThrow('insecure permissions'); }); it('should include chmod hint in error message', async () => { - stat.mockResolvedValue({ mode: 0o100644 }); - await expect(parser.checkFilePermissions('/test')).rejects.toThrow('chmod 600'); + posixStat.mockResolvedValue({ mode: 0o100644 }); + await expect(posixParser.checkFilePermissions('/test')).rejects.toThrow('chmod 600'); }); it('should ignore ENOENT errors', async () => { const err = new Error('not found'); err.code = 'ENOENT'; - stat.mockRejectedValue(err); - await expect(parser.checkFilePermissions('/test')).resolves.not.toThrow(); + posixStat.mockRejectedValue(err); + await expect(posixParser.checkFilePermissions('/test')).resolves.not.toThrow(); }); it('should rethrow other errors', async () => { - stat.mockRejectedValue(new Error('disk failure')); - await expect(parser.checkFilePermissions('/test')).rejects.toThrow('disk failure'); + posixStat.mockRejectedValue(new Error('disk failure')); + await expect(posixParser.checkFilePermissions('/test')).rejects.toThrow('disk failure'); + }); + }); + + describe('checkFilePermissions (Windows)', () => { + it('should skip the permission check without touching stat', async () => { + const win = await loadServerAs('win32'); + const winParser = new win.SSHConfigParser(); + win.fs.stat.mockResolvedValue({ mode: 0o100777 }); + + await expect(winParser.checkFilePermissions('C:\\Users\\test\\.ssh\\config')).resolves.toBeUndefined(); + expect(win.fs.stat).not.toHaveBeenCalled(); }); }); @@ -303,14 +420,18 @@ Host test expect(knownHosts[0].hostname).toBe('10.0.0.1'); }); + // POSIX-pinned: the permission check is a no-op on Windows (see above), so + // asserting that stat() ran only makes sense for the POSIX build. it('should check permissions for configs with passwords', async () => { - readFile + const posix = await loadServerAs('linux'); + const posixParser = new posix.SSHConfigParser(); + posix.fs.readFile .mockResolvedValueOnce(SAMPLE_SSH_CONFIG) .mockResolvedValueOnce(SAMPLE_KNOWN_HOSTS); - stat.mockResolvedValue({ mode: 0o100600 }); + posix.fs.stat.mockResolvedValue({ mode: 0o100600 }); - await parser.getAllKnownHosts(); - expect(stat).toHaveBeenCalled(); + await posixParser.getAllKnownHosts(); + expect(posix.fs.stat).toHaveBeenCalled(); }); it('should work with empty known_hosts', async () => { @@ -476,30 +597,79 @@ describe('SSHClient', () => { }); }); - describe('getAskpassScript', () => { - it('should create askpass script and cache it', async () => { - writeFile.mockResolvedValue(); - chmod.mockResolvedValue(); + // The askpass helper is a /bin/sh script chmod'ed to 700 on POSIX and a .cmd + // batch file on Windows (no chmod — NTFS ACLs, not mode bits). Both variants + // are asserted explicitly so the suite is meaningful on either host OS. + describe('getAskpassScript (POSIX)', () => { + let posixClient; + let posixFs; - const path1 = await client.getAskpassScript(); - const path2 = await client.getAskpassScript(); + beforeEach(async () => { + const posix = await loadServerAs('linux'); + posixClient = new posix.SSHClient(); + posixFs = posix.fs; + posixFs.writeFile.mockResolvedValue(); + posixFs.chmod.mockResolvedValue(); + }); + + it('should create askpass script and cache it', async () => { + const path1 = await posixClient.getAskpassScript(); + const path2 = await posixClient.getAskpassScript(); expect(path1).toBe(path2); - expect(writeFile).toHaveBeenCalledTimes(1); - expect(chmod).toHaveBeenCalledWith(path1, 0o700); + expect(posixFs.writeFile).toHaveBeenCalledTimes(1); + expect(posixFs.chmod).toHaveBeenCalledWith(path1, 0o700); }); it('should write correct script content', async () => { - writeFile.mockResolvedValue(); - chmod.mockResolvedValue(); - - await client.getAskpassScript(); + await posixClient.getAskpassScript(); - expect(writeFile).toHaveBeenCalledWith( + expect(posixFs.writeFile).toHaveBeenCalledWith( expect.stringContaining('mcp-ssh-askpass'), '#!/bin/sh\necho "$MCP_SSH_PASS"\n' ); }); + + it('should use a .sh extension', async () => { + const scriptPath = await posixClient.getAskpassScript(); + expect(scriptPath).toMatch(/mcp-ssh-askpass-\d+\.sh$/); + }); + }); + + describe('getAskpassScript (Windows)', () => { + let winClient; + let winFs; + + beforeEach(async () => { + const win = await loadServerAs('win32'); + winClient = new win.SSHClient(); + winFs = win.fs; + winFs.writeFile.mockResolvedValue(); + winFs.chmod.mockResolvedValue(); + }); + + it('should write a .cmd batch file with CRLF line endings', async () => { + const scriptPath = await winClient.getAskpassScript(); + + expect(scriptPath).toMatch(/mcp-ssh-askpass-\d+\.cmd$/); + expect(winFs.writeFile).toHaveBeenCalledWith( + scriptPath, + '@echo off\r\necho %MCP_SSH_PASS%\r\n' + ); + }); + + it('should not chmod the script', async () => { + await winClient.getAskpassScript(); + expect(winFs.chmod).not.toHaveBeenCalled(); + }); + + it('should cache the script path', async () => { + const path1 = await winClient.getAskpassScript(); + const path2 = await winClient.getAskpassScript(); + + expect(path1).toBe(path2); + expect(winFs.writeFile).toHaveBeenCalledTimes(1); + }); }); describe('buildSpawnEnv', () => { @@ -522,14 +692,17 @@ describe('SSHClient', () => { expect(env.DISPLAY).toBe(process.env.DISPLAY); }); + // POSIX-pinned: relies on the permission check, which is a no-op on Windows. it('should throw if config has insecure permissions', async () => { - readFile.mockResolvedValue(SAMPLE_SSH_CONFIG); - stat.mockResolvedValue({ mode: 0o100644 }); + const posix = await loadServerAs('linux'); + const posixClient = new posix.SSHClient(); + posix.fs.readFile.mockResolvedValue(SAMPLE_SSH_CONFIG); + posix.fs.stat.mockResolvedValue({ mode: 0o100644 }); // Trigger password parsing first - await client.getPasswordForHost('mail'); + await posixClient.getPasswordForHost('mail'); - await expect(client.buildSpawnEnv('mail')).rejects.toThrow('insecure permissions'); + await expect(posixClient.buildSpawnEnv('mail')).rejects.toThrow('insecure permissions'); }); }); @@ -544,7 +717,7 @@ describe('SSHClient', () => { const result = await client.runRemoteCommand('test', 'echo hello'); expect(client._spawn).toHaveBeenCalledWith( - 'ssh', + SSH_BIN, ['-o', 'StrictHostKeyChecking=accept-new', '--', 'test', 'echo hello'], expect.any(Object) ); @@ -583,17 +756,22 @@ describe('SSHClient', () => { expect(result.stderr).toContain('Command timed out'); }); - it('should set detached and env when password is available', async () => { - readFile.mockResolvedValue(SAMPLE_SSH_CONFIG); - stat.mockResolvedValue({ mode: 0o100600 }); - writeFile.mockResolvedValue(); - chmod.mockResolvedValue(); - client._spawn = createMockSpawn({ stdout: 'ok', code: 0 }); + // `detached` is POSIX-only: it exists so ssh talks to SSH_ASKPASS instead of + // grabbing a tty, a problem Windows does not have. Pin the platform on both + // sides so neither expectation depends on the host OS. + it('should set detached and env when password is available (POSIX)', async () => { + const posix = await loadServerAs('linux'); + const posixClient = new posix.SSHClient(); + posix.fs.readFile.mockResolvedValue(SAMPLE_SSH_CONFIG); + posix.fs.stat.mockResolvedValue({ mode: 0o100600 }); + posix.fs.writeFile.mockResolvedValue(); + posix.fs.chmod.mockResolvedValue(); + posixClient._spawn = createMockSpawn({ stdout: 'ok', code: 0 }); - await client.runRemoteCommand('mail', 'ls'); + await posixClient.runRemoteCommand('mail', 'ls'); - expect(client._spawn).toHaveBeenCalledWith( - 'ssh', + expect(posixClient._spawn).toHaveBeenCalledWith( + posix.SSH_BIN, expect.any(Array), expect.objectContaining({ detached: true, @@ -605,13 +783,27 @@ describe('SSHClient', () => { ); }); + it('should set env but not detached when password is available (Windows)', async () => { + const win = await loadServerAs('win32'); + const winClient = new win.SSHClient(); + win.fs.readFile.mockResolvedValue(SAMPLE_SSH_CONFIG); + win.fs.writeFile.mockResolvedValue(); + winClient._spawn = createMockSpawn({ stdout: 'ok', code: 0 }); + + await winClient.runRemoteCommand('mail', 'ls'); + + const opts = winClient._spawn.mock.calls[0][2]; + expect(opts.env).toEqual(expect.objectContaining({ MCP_SSH_PASS: 'killer99' })); + expect(opts.detached).toBeUndefined(); + }); + it('should not set detached without password', async () => { client._spawn = createMockSpawn({ stdout: 'ok', code: 0 }); await client.runRemoteCommand('test', 'ls'); expect(client._spawn).toHaveBeenCalledWith( - 'ssh', + SSH_BIN, expect.any(Array), expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'], @@ -677,7 +869,7 @@ describe('SSHClient', () => { const result = await client.runRemoteCommand('root@test', 'whoami'); expect(client._spawn).toHaveBeenCalledWith( - 'ssh', + SSH_BIN, ['-o', 'StrictHostKeyChecking=accept-new', '--', 'root@test', 'whoami'], expect.any(Object) ); @@ -686,7 +878,9 @@ describe('SSHClient', () => { it('should allow hosts discovered through Include directives', async () => { readFile.mockImplementation(async (filePath) => { - if (String(filePath).endsWith('/config')) return SAMPLE_SSH_CONFIG_WITH_INCLUDE; + // Separator-agnostic: configPath is ~/.ssh/config on POSIX but + // C:\Users\…\.ssh\config on Windows, where endsWith('/config') misses. + if (/[\\/]config$/.test(String(filePath))) return SAMPLE_SSH_CONFIG_WITH_INCLUDE; if (String(filePath).endsWith('.conf')) return `Host included\n HostName 10.10.10.10\n`; if (String(filePath).endsWith('known_hosts')) return ''; return ''; @@ -789,7 +983,7 @@ describe('SSHClient', () => { const result = await client.uploadFile('test', '/local/file', '/remote/file'); expect(result).toBe(true); expect(client._execFileAsync).toHaveBeenCalledWith( - 'scp', + SCP_BIN, ['-o', 'StrictHostKeyChecking=accept-new', '--', '/local/file', 'test:/remote/file'], expect.any(Object) ); @@ -846,7 +1040,7 @@ describe('SSHClient', () => { const result = await client.downloadFile('test', '/remote/file', '/local/file'); expect(result).toBe(true); expect(client._execFileAsync).toHaveBeenCalledWith( - 'scp', + SCP_BIN, ['-o', 'StrictHostKeyChecking=accept-new', '--', 'test:/remote/file', '/local/file'], expect.any(Object) ); @@ -1000,10 +1194,28 @@ describe('SSHClient', () => { describe('MCP Server Handlers', () => { let server; let handlers; + let clientSpies; + + afterEach(() => { + for (const spy of clientSpies) spy.mockRestore(); + }); beforeEach(async () => { vi.clearAllMocks(); + // These tests drive the real SSHClient that main() constructs, so every tool + // call would otherwise spawn an actual ssh/scp process against 1.2.3.4 and + // block on the network. Stubbing the three process-starting methods keeps + // the block a dispatch test — which is all it asserts — instead of a slow, + // network-dependent one that blows the 5s timeout on Windows CI runners. + // The methods themselves are covered by the SSHClient tests above. + clientSpies = [ + vi.spyOn(SSHClient.prototype, 'runRemoteCommand') + .mockResolvedValue({ stdout: 'connected', stderr: '', code: 0 }), + vi.spyOn(SSHClient.prototype, 'uploadFile').mockResolvedValue(true), + vi.spyOn(SSHClient.prototype, 'downloadFile').mockResolvedValue(true), + ]; + // Capture the request handlers that main() registers handlers = {}; @@ -1095,15 +1307,52 @@ describe('MCP Server Handlers', () => { it('should cap runRemoteCommand timeout at 300000ms', async () => { readFile.mockResolvedValue(`Host test\n HostName 1.2.3.4\n`); + const spy = vi.spyOn(SSHClient.prototype, 'runRemoteCommand') + .mockResolvedValue({ stdout: '', stderr: '', code: 0 }); + + try { + await handlers.callTool({ + params: { + name: 'runRemoteCommand', + arguments: { hostAlias: 'test', command: 'echo hi', timeout: 999999 } + } + }); - const result = await handlers.callTool({ - params: { - name: 'runRemoteCommand', - arguments: { hostAlias: 'test', command: 'echo hi', timeout: 999999 } - } - }); + expect(spy).toHaveBeenCalledWith('test', 'echo hi', { timeout: 300000 }); + } finally { + spy.mockRestore(); + } + }); + + it('should default the runRemoteCommand timeout to 120000ms', async () => { + readFile.mockResolvedValue(`Host test\n HostName 1.2.3.4\n`); + const spy = vi.spyOn(SSHClient.prototype, 'runRemoteCommand') + .mockResolvedValue({ stdout: '', stderr: '', code: 0 }); + + try { + await handlers.callTool({ + params: { name: 'runRemoteCommand', arguments: { hostAlias: 'test', command: 'echo hi' } } + }); - expect(result.content[0].type).toBe('text'); + expect(spy).toHaveBeenCalledWith('test', 'echo hi', { timeout: 120000 }); + } finally { + spy.mockRestore(); + } + }); + + it('should stringify non-Error values thrown by a tool', async () => { + const spy = vi.spyOn(SSHClient.prototype, 'listKnownHosts') + .mockRejectedValue('a bare string, not an Error'); + + try { + const result = await handlers.callTool({ + params: { name: 'listKnownHosts', arguments: {} } + }); + + expect(JSON.parse(result.content[0].text).error).toBe('a bare string, not an Error'); + } finally { + spy.mockRestore(); + } }); it('should handle checkConnectivity tool call', async () => { @@ -1196,3 +1445,410 @@ describe('main() error handling', () => { exitSpy.mockRestore(); }); }); + +// ============================================================================= +// Platform-specific module initialisation +// +// resolveExecutable() runs once at import time and only does real work on +// Windows, where spawn() is called with shell:false and therefore cannot rely +// on PATH lookup. Exercised here by re-importing the module with the platform +// and PATH/PATHEXT faked, so the Windows resolution logic is covered from any +// host OS. +// ============================================================================= + +describe('resolveExecutable (Windows binary resolution)', () => { + let binDir; + + beforeAll(() => { + binDir = mkdtempSync(join(tmpdir(), 'mcp-ssh-bin-')); + writeFileSync(join(binDir, 'ssh.EXE'), ''); + writeFileSync(join(binDir, 'scp.EXE'), ''); + // A directory named like the executable must not be mistaken for one. + mkdirSync(join(binDir, 'ssh.CMD')); + }); + + afterAll(() => { + rmSync(binDir, { recursive: true, force: true }); + }); + + it('should resolve ssh/scp to absolute paths found on PATH', async () => { + const win = await loadServerAs('win32', { + // Trailing separator produces an empty entry, which must be skipped. + PATH: `${binDir};`, + PATHEXT: '.EXE;.CMD', + }); + + expect(win.SSH_BIN).toBe(join(binDir, 'ssh.EXE')); + expect(win.SCP_BIN).toBe(join(binDir, 'scp.EXE')); + }); + + it('should skip directory entries that match the name but are not files', async () => { + const win = await loadServerAs('win32', { + PATH: binDir, + PATHEXT: '.CMD;.EXE', // .CMD first: ssh.CMD is a directory, not a match + }); + + expect(win.SSH_BIN).toBe(join(binDir, 'ssh.EXE')); + }); + + it('should fall back to a bare .exe name when PATH holds no match', async () => { + const emptyDir = mkdtempSync(join(tmpdir(), 'mcp-ssh-empty-')); + try { + const win = await loadServerAs('win32', { PATH: emptyDir, PATHEXT: '.EXE' }); + + expect(win.SSH_BIN).toBe('ssh.exe'); + expect(win.SCP_BIN).toBe('scp.exe'); + } finally { + rmSync(emptyDir, { recursive: true, force: true }); + } + }); + + it('should fall back when PATH is unset entirely', async () => { + const win = await loadServerAs('win32', { PATH: undefined, PATHEXT: '.EXE' }); + expect(win.SSH_BIN).toBe('ssh.exe'); + }); + + it('should use the default PATHEXT list when the variable is unset', async () => { + const win = await loadServerAs('win32', { PATH: binDir, PATHEXT: undefined }); + // .EXE is part of the built-in default list. + expect(win.SSH_BIN).toBe(join(binDir, 'ssh.EXE')); + }); + + it('should use the bare name on POSIX, letting spawn search PATH', async () => { + const posix = await loadServerAs('linux', { PATH: binDir }); + + expect(posix.SSH_BIN).toBe('ssh'); + expect(posix.SCP_BIN).toBe('scp'); + }); +}); + +// ============================================================================= +// ssh-config value normalization edge cases +// +// extractHostsFromConfig is a pure function over the parser's section array, so +// these feed it shapes that ssh-config can emit but that are awkward to produce +// from config text alone. +// ============================================================================= + +describe('config value normalization', () => { + let parser; + + beforeEach(() => { + parser = new SSHConfigParser(); + vi.clearAllMocks(); + }); + + it('should treat a directive without a value as absent', () => { + const hosts = parser.extractHostsFromConfig([ + { + param: 'Host', + value: 'x', + config: [ + { param: 'HostName', value: '1.2.3.4' }, + { param: 'SendEnv', value: null }, + ], + }, + ], '/test'); + + expect(hosts).toHaveLength(1); + expect(hosts[0].sendenv).toBe(''); + }); + + it('should accept plain strings inside a multi-token value', () => { + // ssh-config normally yields {val,…} token objects, but a hand-built or + // future-shaped array of bare strings must normalize the same way. + const hosts = parser.extractHostsFromConfig([ + { + param: 'Host', + value: ['first', 'second'], + config: [{ param: 'HostName', value: '1.2.3.4' }], + }, + ], '/test'); + + expect(hosts[0].aliases).toEqual(['first', 'second']); + expect(hosts[0].alias).toBe('first'); + }); + + it('should skip a Host block whose value is empty', () => { + const hosts = parser.extractHostsFromConfig([ + { param: 'Host', value: [], config: [{ param: 'HostName', value: '1.2.3.4' }] }, + ], '/test'); + + expect(hosts).toEqual([]); + }); + + it('should ignore top-level directives that are not Host or Include', () => { + const config = sshConfigLib.parse(` +ServerAliveInterval 30 + +Host real + HostName 1.2.3.4 +`); + const hosts = parser.extractHostsFromConfig(config, '/test'); + + expect(hosts).toHaveLength(1); + expect(hosts[0].alias).toBe('real'); + }); +}); + +// ============================================================================= +// expandIncludePath — paths that actually exist +// ============================================================================= + +describe('expandIncludePath (existing paths)', () => { + let parser; + let dir; + + beforeAll(() => { + dir = mkdtempSync(join(tmpdir(), 'mcp-ssh-inc-')); + writeFileSync(join(dir, 'included.conf'), 'Host inc\n HostName 5.5.5.5\n'); + }); + + afterAll(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + beforeEach(() => { + parser = new SSHConfigParser(); + }); + + it('should return an existing absolute path', () => { + const target = join(dir, 'included.conf'); + expect(parser.expandIncludePath(target, '/base/config')).toEqual([target]); + }); + + it('should expand a glob pattern to the files it matches', () => { + // glob patterns are forward-slash based on every platform, including Windows. + const pattern = `${dir.replace(/\\/g, '/')}/*.conf`; + const result = parser.expandIncludePath(pattern, '/base/config'); + + expect(result).toHaveLength(1); + expect(result[0]).toMatch(/included\.conf$/); + }); +}); + +// ============================================================================= +// Remaining branches: argument validation, known_hosts matching, silent mode, +// askpass cleanup handlers and the tool-dispatch catch-all. +// ============================================================================= + +describe('_assertSafeHostAlias argument validation', () => { + let client; + + beforeEach(() => { + client = new SSHClient(); + vi.clearAllMocks(); + }); + + it.each([ + ['undefined', undefined], + ['null', null], + ['a number', 42], + ['an empty string', ''], + ['an array', ['test']], + ])('should reject %s before touching ssh', async (_label, value) => { + expect(() => client._assertSafeHostAlias(value)).toThrow('must be a non-empty string'); + }); + + it('should surface the type error through runRemoteCommand', async () => { + client._spawn = createMockSpawn({ stdout: 'ok', code: 0 }); + + await expect(client.runRemoteCommand(null, 'echo hi')).rejects.toThrow( + 'must be a non-empty string' + ); + expect(client._spawn).not.toHaveBeenCalled(); + }); +}); + +describe('hostMatchesAlias against known_hosts entries', () => { + let client; + + beforeEach(() => { + client = new SSHClient(); + vi.clearAllMocks(); + }); + + it('should keep scanning past a non-matching known_hosts entry', async () => { + // known_hosts entries carry only a hostname (no alias/aliases), so matching + // them falls through to the plain-alias comparison. The host we ask for is + // the *second* entry, so the first one has to be rejected and skipped. + readFile.mockImplementation(async (filePath) => { + if (/known_hosts$/.test(String(filePath))) { + return '10.0.0.1 ssh-ed25519 AAAA...\n10.0.0.2 ssh-ed25519 BBBB...\n'; + } + return `Host other\n HostName 192.168.1.1\n`; + }); + client._spawn = createMockSpawn({ stdout: 'ok\n', code: 0 }); + + const result = await client.runRemoteCommand('10.0.0.2', 'uptime'); + expect(result.code).toBe(0); + }); +}); + +describe('silent mode', () => { + it('should suppress debug output when MCP_SILENT is set', async () => { + const silent = await loadServerAs('linux', { MCP_SILENT: 'true' }); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + try { + silent.debugLog('this must not be written\n'); + expect(writeSpy).not.toHaveBeenCalled(); + } finally { + writeSpy.mockRestore(); + } + }); + + it('should write debug output when MCP_SILENT is not set', async () => { + const loud = await loadServerAs('linux', { MCP_SILENT: undefined }); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + try { + loud.debugLog('hello\n'); + expect(writeSpy).toHaveBeenCalledWith('hello\n'); + } finally { + writeSpy.mockRestore(); + } + }); +}); + +describe('askpass script cleanup handlers', () => { + let client; + let exitSpy; + + beforeEach(async () => { + vi.clearAllMocks(); + writeFile.mockResolvedValue(); + chmod.mockResolvedValue(); + client = new SSHClient(); + exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}); + }); + + afterEach(() => { + exitSpy.mockRestore(); + }); + + // The handlers registered by getAskpassScript are invoked directly: they only + // ever run while the process is tearing down, which a unit test cannot trigger. + async function registerAndTake(signal) { + const before = process.listeners(signal).length; + await client.getAskpassScript(); + const listeners = process.listeners(signal); + expect(listeners.length).toBeGreaterThan(before); + const handler = listeners[listeners.length - 1]; + return () => { + handler(); + process.removeListener(signal, handler); + }; + } + + it('should unlink the script on exit', async () => { + const run = await registerAndTake('exit'); + // unlinkSync throws ENOENT (writeFile is mocked, so no file exists) and the + // handler must swallow it. + expect(run).not.toThrow(); + }); + + it('should clean up and exit 130 on SIGINT', async () => { + const run = await registerAndTake('SIGINT'); + run(); + expect(exitSpy).toHaveBeenCalledWith(130); + }); + + it('should clean up and exit 143 on SIGTERM', async () => { + const run = await registerAndTake('SIGTERM'); + run(); + expect(exitSpy).toHaveBeenCalledWith(143); + }); +}); + +describe('password env on the scp paths', () => { + let client; + + beforeEach(() => { + client = new SSHClient(); + vi.clearAllMocks(); + readFile.mockResolvedValue(SAMPLE_SSH_CONFIG); + stat.mockResolvedValue({ mode: 0o100600 }); + writeFile.mockResolvedValue(); + chmod.mockResolvedValue(); + }); + + it('should pass password env to downloadFile', async () => { + client._execFileAsync = createMockExecFileAsync(); + + const result = await client.downloadFile('mail', '/remote/file', '/local/file'); + + expect(result).toBe(true); + expect(client._execFileAsync).toHaveBeenCalledWith( + SCP_BIN, + expect.any(Array), + expect.objectContaining({ + env: expect.objectContaining({ MCP_SSH_PASS: 'killer99' }), + }) + ); + }); + + it('should skip the permission sweep when no config declared a password', async () => { + // A password reached us without extractHostsFromConfig having flagged any + // config file — there is then nothing to check the permissions of. + client.getPasswordForHost = vi.fn().mockResolvedValue('secret'); + client.configParser._configsWithPasswords = undefined; + + const env = await client.buildSpawnEnv('anything'); + + expect(env.MCP_SSH_PASS).toBe('secret'); + expect(stat).not.toHaveBeenCalled(); + }); +}); + +describe('output truncation', () => { + let client; + + beforeEach(() => { + client = new SSHClient(); + vi.clearAllMocks(); + readFile.mockResolvedValue(`Host test\n HostName 1.2.3.4\n`); + }); + + // Three chunks: the second crosses the limit and appends the marker, the third + // must be dropped silently rather than appending it again. + function spawnEmitting(stream, chunks) { + return vi.fn(() => { + const child = new EventEmitter(); + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + child.kill = vi.fn(); + + setTimeout(() => { + for (const chunk of chunks) child[stream].emit('data', Buffer.from(chunk)); + child.emit('close', 0); + }, 5); + + return child; + }); + } + + it('should append the stdout truncation marker only once', async () => { + client._spawn = spawnEmitting('stdout', [ + 'x'.repeat(10 * 1024 * 1024), + 'y'.repeat(1024), + 'z'.repeat(1024), + ]); + + const result = await client.runRemoteCommand('test', 'bigcmd'); + const markers = result.stdout.match(/\[Output truncated/g) || []; + expect(markers).toHaveLength(1); + }); + + it('should append the stderr truncation marker only once', async () => { + client._spawn = spawnEmitting('stderr', [ + 'x'.repeat(10 * 1024 * 1024), + 'y'.repeat(1024), + 'z'.repeat(1024), + ]); + + const result = await client.runRemoteCommand('test', 'bigcmd'); + const markers = result.stderr.match(/\[Stderr truncated/g) || []; + expect(markers).toHaveLength(1); + }); +}); diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 0000000..50967da --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,27 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Windows CI runners are markedly slower than a dev machine (the suite takes + // ~0.5s locally but ~15s there), and a few tests allocate 10MB buffers or + // re-import the module graph. The 5s default left too little headroom. + testTimeout: 15000, + coverage: { + provider: 'v8', + // server.mjs is the whole production implementation (it is deliberately + // self-contained — see CLAUDE.md). bin/mcp-ssh.js is excluded: it is a + // top-level-await wrapper whose only job is to call main(), so importing + // it in a unit test would start a real MCP server on STDIO. + include: ['server.mjs'], + reporter: ['text', 'json-summary', 'html'], + // The suite covers every line, branch and function of server.mjs, on both + // the POSIX and the Windows code paths. Keep it that way. + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + }, +});