From ac3bb5cdabf8fd6729a7f6802cc2c2b125459dff Mon Sep 17 00:00:00 2001 From: retn0 Date: Thu, 3 Sep 2026 11:40:34 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Improve?= =?UTF-8?q?=20code=20quality=20foundation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 10 +- eslint.config.js | 9 + oxfmt.config.ts | 7 + oxlint.config.ts | 3 + package.json | 37 +- pnpm-lock.yaml | 2942 +++++++++++++++++++- scripts/clean-dist.mjs | 12 +- src/auth/provider.ts | 4 +- src/auth/single-user-provider.ts | 10 +- src/cli/main.ts | 32 +- src/codexpro/client-pool.ts | 165 +- src/codexpro/tool-manifest.ts | 14 +- src/config.ts | 36 +- src/domain/ids.ts | 8 +- src/domain/types.ts | 14 +- src/mcp/compatibility.ts | 14 +- src/mcp/control-server.ts | 248 +- src/mcp/gateway.ts | 74 +- src/mcp/main.ts | 43 +- src/sandbox/sbx-driver.ts | 208 +- src/sandbox/service.ts | 156 +- src/state/database.ts | 182 +- src/workspaces/policy.ts | 39 +- src/workspaces/service.ts | 116 +- test/README.md | 51 + test/client-pool.test.ts | 31 - test/compatibility.test.ts | 25 - test/config.test.ts | 13 - test/e2e/sandbox-lifecycle.test.ts | 266 ++ test/gateway.test.ts | 76 - test/integration/gateway.test.ts | 230 ++ test/integration/sandbox-service.test.ts | 249 ++ test/integration/workspace-service.test.ts | 73 + test/sandbox-service.test.ts | 220 -- test/sbx-integration.test.ts | 148 - test/tool-manifest.test.ts | 16 - test/unit/client-pool.test.ts | 35 + test/unit/compatibility.test.ts | 24 + test/unit/config.test.ts | 12 + test/unit/tool-manifest.test.ts | 18 + test/workspace-service.test.ts | 63 - tsconfig.build.json | 7 + tsconfig.json | 10 +- vitest.config.ts | 37 + 45 files changed, 4886 insertions(+), 1102 deletions(-) create mode 100644 eslint.config.js create mode 100644 oxfmt.config.ts create mode 100644 oxlint.config.ts create mode 100644 test/README.md delete mode 100644 test/client-pool.test.ts delete mode 100644 test/compatibility.test.ts delete mode 100644 test/config.test.ts create mode 100644 test/e2e/sandbox-lifecycle.test.ts delete mode 100644 test/gateway.test.ts create mode 100644 test/integration/gateway.test.ts create mode 100644 test/integration/sandbox-service.test.ts create mode 100644 test/integration/workspace-service.test.ts delete mode 100644 test/sandbox-service.test.ts delete mode 100644 test/sbx-integration.test.ts delete mode 100644 test/tool-manifest.test.ts create mode 100644 test/unit/client-pool.test.ts create mode 100644 test/unit/compatibility.test.ts create mode 100644 test/unit/config.test.ts create mode 100644 test/unit/tool-manifest.test.ts delete mode 100644 test/workspace-service.test.ts create mode 100644 tsconfig.build.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 0c70f93..15520bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist/ +coverage/ node_modules/ .env .env.* diff --git a/README.md b/README.md index 4a8fc54..205debd 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,17 @@ Destroying an active sandbox follows the same workspace policy, so a managed wor ## Setup -Requirements are Node.js 22 or newer, pnpm, Docker Sandboxes (`sbx`), and the previously installed Secure MCP Tunnel client. +Requirements are Node.js 24 or newer, pnpm, Docker Sandboxes (`sbx`), and the previously installed Secure MCP Tunnel client. ```bash pnpm install ./scripts/setup-template.sh pnpm check -pnpm test:integration +pnpm test:e2e ``` +`pnpm check` is the normal development and CI quality gate: formatting, linting, typechecking, unit and integration tests, and the production build. It deliberately excludes real Docker Sandbox E2E tests. Run `pnpm test:e2e` on a trusted host with `sbx` and the local CodexPro template installed. See [`test/README.md`](./test/README.md) for the test boundaries and individual commands. + `setup-template.sh` creates the local `chat2shell-codexpro:0.30.0` sandbox template once. The template contains CodexPro and its npm dependencies, but no workspace, application source, credentials, or tunnel secret. @@ -146,13 +148,13 @@ Unexported changes in a private clone disappear when its sandbox is destroyed, s ## MCP workflow ```json -{"workspace_mode":"managed"} +{ "workspace_mode": "managed" } ``` Pass the returned sandbox ID to every CodexPro tool: ```json -{"sandbox_id":"sbx_...","command":"pnpm test"} +{ "sandbox_id": "sbx_...", "command": "pnpm test" } ``` To view a web application, start it on every sandbox interface and expose its port: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a81430b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,9 @@ +import retn0 from '@retn0/eslint-config'; +import eslintConfigOxlint from '@retn0/eslint-config-oxlint'; + +export default retn0( + { + environments: ['node'], + }, + eslintConfigOxlint, +); diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..61f0e41 --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,7 @@ +import config from '@retn0/oxfmt-config'; +import { defineConfig } from 'oxfmt'; + +export default defineConfig({ + ...config, + ignorePatterns: [...(config.ignorePatterns ?? []), 'src/codexpro/standard-tools.json'], +}); diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..b51ada9 --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,3 @@ +import retn0 from '@retn0/oxlint-config'; + +export default retn0(); diff --git a/package.json b/package.json index eafd2b4..5de1c31 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,21 @@ "private": true, "description": "A private MCP control plane for isolated shell and sandbox access from ChatGPT.", "type": "module", - "packageManager": "pnpm@11.23.0", - "engines": { - "node": ">=22" - }, "scripts": { - "build": "pnpm clean && tsc -p tsconfig.json", + "build": "pnpm clean && tsc -p tsconfig.build.json", "clean": "node scripts/clean-dist.mjs", "cli": "node dist/src/cli/main.js", - "check": "pnpm typecheck && pnpm test", + "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build", + "format": "oxfmt .", + "format:check": "oxfmt --check .", + "lint": "oxlint . && eslint .", "start": "node dist/src/mcp/main.js", - "test": "pnpm build && node --test dist/test/**/*.test.js", - "test:integration": "pnpm build && CHAT2SHELL_RUN_SBX_INTEGRATION=1 node --test dist/test/sbx-integration.test.js", + "test": "vitest run --project unit --project integration", + "test:coverage": "vitest run --coverage --project unit --project integration", + "test:e2e": "vitest run --project e2e", + "test:integration": "vitest run --project integration", + "test:unit": "vitest run --project unit", + "test:watch": "vitest --project unit --project integration", "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { @@ -23,7 +26,21 @@ "zod": "3.25.76" }, "devDependencies": { + "@retn0/eslint-config": "2.1.0", + "@retn0/eslint-config-oxlint": "0.1.2", + "@retn0/oxfmt-config": "0.2.0", + "@retn0/oxlint-config": "2.2.0", "@types/node": "^24.0.0", - "typescript": "^5.9.0" - } + "@vitest/coverage-v8": "4.1.11", + "eslint": "10.7.0", + "oxfmt": "0.59.0", + "oxlint": "1.73.0", + "oxlint-tsgolint": "0.25.0", + "typescript": "^5.9.0", + "vitest": "4.1.11" + }, + "engines": { + "node": ">=24.0.0" + }, + "packageManager": "pnpm@11.23.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb92ee5..b4d075b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,26 +10,251 @@ importers: dependencies: '@modelcontextprotocol/sdk': specifier: 1.30.0 - version: 1.30.0(zod@3.25.76) + version: 1.30.0(supports-color@7.2.0)(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 devDependencies: + '@retn0/eslint-config': + specifier: 2.1.0 + version: 2.1.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@retn0/eslint-config-oxlint': + specifier: 0.1.2 + version: 0.1.2(eslint@10.7.0(supports-color@7.2.0))(oxlint@1.73.0(oxlint-tsgolint@0.25.0)) + '@retn0/oxfmt-config': + specifier: 0.2.0 + version: 0.2.0(oxfmt@0.59.0) + '@retn0/oxlint-config': + specifier: 2.2.0 + version: 2.2.0(oxlint-tsgolint@0.25.0)(oxlint@1.73.0(oxlint-tsgolint@0.25.0)) '@types/node': specifier: ^24.0.0 version: 24.13.3 + '@vitest/coverage-v8': + specifier: 4.1.11 + version: 4.1.11(vitest@4.1.11) + eslint: + specifier: 10.7.0 + version: 10.7.0(supports-color@7.2.0) + oxfmt: + specifier: 0.59.0 + version: 0.59.0 + oxlint: + specifier: 1.73.0 + version: 1.73.0(oxlint-tsgolint@0.25.0) + oxlint-tsgolint: + specifier: 0.25.0 + version: 0.25.0 typescript: specifier: ^5.9.0 version: 5.9.3 + vitest: + specifier: 4.1.11 + version: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)) packages: + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-react/ast@5.17.3': + resolution: {integrity: sha512-qcSUVoHXcnCU2chumBSOPpYsTXho4Wxby7gqfQU/dVv7gFgQojTWgACdWqolc5biVmNfnadnmqeMYwwrt/9MEw==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint-react/core@5.17.3': + resolution: {integrity: sha512-DcjDlcaiGjTFZn6uPwNsojG6inY7mynExQrG8SOE4vUkzBl7BLKnT+DCAr9IfqJMoLMxEIdM9Z0Fci3vz+hM0A==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint-react/eslint-plugin@5.17.3': + resolution: {integrity: sha512-aoo0gvZev8KISD2oN+saXzPxWYYKxIDHo9/1c5GpPSkHYSpa3p7uNotGLPl2oVCVtJhM7vFt2Z6ukKv4+5Ol5A==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint-react/eslint@5.17.3': + resolution: {integrity: sha512-A05l72MjcpGnxEd3Urd1RVWFSyLc2AH43nGlfl5nv+meTZqJBnOlE21Pe18kAPF6zTXDGztu4L2iTbPBxbDJKQ==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint-react/jsx@5.17.3': + resolution: {integrity: sha512-toDBxmkcMPMX3I1pAwdECIMrChlRA2tDZLHNUfg6ZkX+f6lbKYsCwLyKFrsCI2iH1U22WjEsfPmF4YnRJmlerQ==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint-react/shared@5.17.3': + resolution: {integrity: sha512-ZtQjvT8/a1+NQ8I1xX8/V+0ZxKwBsTTmW4zXOFE3ZUqNA2wC4LgFndw3v5UMkcB3LWIWCottkOXJcCQ8cQy5qg==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint-react/var@5.17.3': + resolution: {integrity: sha512-oTN+7kI8ZN3a+ADz+L1SwiXL59CIogCukc8EWe/vj+8iXPQ6sS7dER41ySfk2Q3bOUb+lE7HSp6oLWjvrA/xAg==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@hono/node-server@2.1.1': resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} engines: {node: '>=20'} peerDependencies: hono: ^4 + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@modelcontextprotocol/sdk@1.30.0': resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} engines: {node: '>=18'} @@ -40,13 +265,590 @@ packages: '@cfworker/json-schema': optional: true + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + + '@oxfmt/binding-android-arm-eabi@0.59.0': + resolution: {integrity: sha512-bNTnfbuG7sAwb2PakMNaDukx5kXeW9duXOBeWtTOiLz3fXz3q2DlWguufPZ+c2IHEVrRXHD+M4aUgEWm841LDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.59.0': + resolution: {integrity: sha512-R/Sn7z52QtdAKNqQLLY0EK7hVMjXiz3XUlvoCFCm/60jgIzAnQtiqLKBCFaBkimCQL5rs2ezPMcicpjCsrl54Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.59.0': + resolution: {integrity: sha512-vm/ynUqE4HjC0ZIEjmXv1UJu1/GngccQ+T+TJudTMxUxm6r+GQTg1TO3E5jJfI71pBaXxSzs1+vWHIwuilGHhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.59.0': + resolution: {integrity: sha512-uTtYDpLN/obfKVWGpgEc8BqYlLZBQTPz2uYEvLRy3HPZxjZ34wiFzukUBU2bf64JuCYZI//GTV1EOMmWlPjf/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.59.0': + resolution: {integrity: sha512-e2UnxL/ifStSPy8ffBCDbdy595SYsGy+U1pur4G65TuMmWxAMBzYGG7atZo/3mp515p8rZdsflxVD/E1FAdPLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.59.0': + resolution: {integrity: sha512-LtdeZ1l0urxte3VNi3g8cocZwv1xGM1NKHSgF/fJEEVhyQmlgGh7WFWKFd/pNuO7djfvPNtNO1+MS+FEWkgVSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.59.0': + resolution: {integrity: sha512-dBTciSsj9GTMl7p+h2gMSI0hoPn2ijfc/dUsbnWsP0RbwgPl2r0C/5zkMb3Pb+gGj17LH7f1o4qLo9aes/pAvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.59.0': + resolution: {integrity: sha512-tXVdJ/JINsNWdponPHN0OuKHtC+HdpyoS9sd6IDPNiiEYsRki8b7tefRZ1iMnRkdbyT4SEbguWsr6o+5awvbPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.59.0': + resolution: {integrity: sha512-RRTq38i2zT5fnw6XGHjvT6w2mh6x/G3m6AZcAZ56OTDTT/lsOeYnG3SVjwmH40z5kPqF+lf+o35e6m6PpKy9Dw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.59.0': + resolution: {integrity: sha512-lD3k7glAJSaXW0D6xzu8VOZbYbosvy+0ktOVkfLEoQF5HJlMSxTQ2KNW0JO+08ccP/1ElOKktVEMI0fqRbVB4w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.59.0': + resolution: {integrity: sha512-WH5ZP1RbuHKBO/yfPRQKpNO/ijHcEDNbnmC4VPf/Bcd3+mbMAZpRiJWRa1PL5bREdIZZHo343mk3sqlc9x7Usw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.59.0': + resolution: {integrity: sha512-743wOiaI9RZY4QVGkWkfGRavD5ZJUJ6gscFjVrVu1dP8AZh9jM+a6v3NhlR+OIzHdS6DhLM96w+gcVskskz7rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.59.0': + resolution: {integrity: sha512-xjRXQsRnrRZCcCkIEnbd2lmsQNobtwwkJxdy2bWXhZ1lIN0ouZwsBXRsoovW3yATuziAYwr9HMiQuR/Cc75NIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.59.0': + resolution: {integrity: sha512-4hNjqq/Rbr9B+StY9zMMAfm72+mtM4v80xYL5Qkb59Qd72g2vJMI0iFlPj3kf6miMsie/yJ7rt4urJT292HBgA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.59.0': + resolution: {integrity: sha512-NH579iN8EVQYsWowUB8B5vFchcylJtwPVJ7NmUAqEQHNLfhPbDT3K56KrECNAkUN4QpF4qiMgN2vsfZwVvjm7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.59.0': + resolution: {integrity: sha512-mzZy3Z5Aj1D75Aq9FVlmoRQH5ei8Ga4o/NZmlXkKyeZ5EmPrUXRR7c6BMBteV1ZuZ/356UYDuLRLjAMxTDTiBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.59.0': + resolution: {integrity: sha512-0CpDJ1gE3jN1Gk6xms1Ie6LPfPcOtY4FAtoOmVLHQoAf8DvO2wd0DW2dIX2f7YTp5dxrr0ND8JeUEjm3DP3k5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.59.0': + resolution: {integrity: sha512-zwdKBu3pt87uW0bRcywZb0oGMS7C6n87qogwRYFUgmk44T90ZzYlPjtlFYXs/DnBFrgNCvlHwCuWKfVWLeE7kw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.59.0': + resolution: {integrity: sha512-dUUbZkKgWrmAeI/puzv4bxN8lzcYaFnQVwFTFtwO2Gp8M7lZGSE2qJjC58g518+1bltJ8mizjYwD0BGHym0l/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.25.0': + resolution: {integrity: sha512-87opKlwFP8qS9WHAeETV+kA0fC9Oyj4sg7OxWdI4xQY0WC7zlN6BgG66uE5mvtN5mahkt/gL0i/AVEnX6POq2Q==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.25.0': + resolution: {integrity: sha512-HJmuZexsrhqp4WmETn+Soq7Ogt5F0jirv+cYRSniIPe+d/x5beQzLX69xOLhQRE+8FLGETe7FahWMVP8x0dW4g==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.25.0': + resolution: {integrity: sha512-aNyYsPREvCJi3qjfBA0sQB7DhT3y/W5Ac2JI2D8IJynoTOAhVZj401Si6901oDajlBWyqJqqojudn0VgHB6+7A==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.25.0': + resolution: {integrity: sha512-+60+VjK9Mch3uA5WlTdNHuAm5+WA7wPPjuWdPWlU0F6JJpYpGZXUpO1RPKuFEWsBpNbLcLeJ0LbCJ1doWu58NA==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.25.0': + resolution: {integrity: sha512-r53TO+eHp/t53nnUkQJfrYYXODPAxmtf3RUFQG5XsE2hD21IunliOaAdZXP2UwzCx+r/fbNaEelqTaAHcDr57w==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.25.0': + resolution: {integrity: sha512-vqe66B+gL9HarhyHemdlfC2VWT7eoA+o/ufZ7zT6AGHv64boyDZIJS3U+rpZo+ey4O7wZtiS/vYR2fWqDoFeBw==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.73.0': + resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.73.0': + resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.73.0': + resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.73.0': + resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.73.0': + resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.73.0': + resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.73.0': + resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.73.0': + resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.73.0': + resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.73.0': + resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@retn0/eslint-config-oxlint@0.1.2': + resolution: {integrity: sha512-p4UOml78poliyXO9VDlTmVW6eR0MCOaaUSuZYDQ4kDVU4ViY2XTpfBDsADj9KOdvNyPxaOD/F9AI+/POdgwy+g==} + engines: {node: '>=24.0.0'} + peerDependencies: + eslint: ^10.0.0 + oxlint: ~1.73.0 + + '@retn0/eslint-config@2.1.0': + resolution: {integrity: sha512-d+1kjP4/E+2ejRKjrnroZvR4QHVjkWrhzyH7J2P3TfJbxbDFi0bLZiqP+LLpMFpLzaCX0sQF/CLirHZKZBxNGA==} + engines: {node: '>=24.0.0'} + peerDependencies: + eslint: ^10.0.0 + + '@retn0/oxfmt-config@0.2.0': + resolution: {integrity: sha512-OIPw5xTdOUaRGzGAJa78Y/wnWUZwBkU3F+3mObeXDb61Y2ONldOoii6Iynj5RTqEzR4BQoLwBO+Fh1CNtZXONQ==} + engines: {node: '>=24.0.0'} + peerDependencies: + oxfmt: '>=0.59.0 <0.60.0' + + '@retn0/oxlint-config@2.2.0': + resolution: {integrity: sha512-rBF6t1PtC9Gyj3LU1BT1I/+xFL9ZzPi91ezBDGhOPPMNSZJF/Kw3UkhFr9UnWCdeWj7gdbChrErk1SnC5QbEfg==} + engines: {node: '>=24.0.0'} + peerDependencies: + oxlint: ^1.73.0 + oxlint-tsgolint: '>=0.25.0 <0.26.0' + + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stylistic/eslint-plugin@5.10.0': + resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@typescript-eslint/eslint-plugin@8.64.0': + resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.64.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.64.0': + resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.69.0': + resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.64.0': + resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.69.0': + resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/tsconfig-utils@8.69.0': + resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.64.0': + resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.69.0': + resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.69.0': + resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/typescript-estree@8.69.0': + resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.64.0': + resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.69.0': + resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.69.0': + resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/coverage-v8@4.1.11': + resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} + peerDependencies: + '@vitest/browser': 4.1.11 + vitest: 4.1.11 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -55,13 +857,44 @@ packages: ajv: optional: true + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.20: + resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} + engines: {node: '>=6.0.0'} + hasBin: true + + birecord@0.1.2: + resolution: {integrity: sha512-5PAPTTmMpMEb+GuMb5DebfBkipRGyIW9+gtwEBSoDA9xkhHILm04+hZQ702pMksu3d8YAuGkmgTzQWcKqTPScA==} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -74,6 +907,16 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -86,6 +929,9 @@ packages: resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -111,10 +957,17 @@ packages: supports-color: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -122,6 +975,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.420: + resolution: {integrity: sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -134,13 +990,136 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-oxlint@1.73.0: + resolution: {integrity: sha512-2qsGYkwpas99+jLmB7F3W8Gv+7QkHIr67AMw10UTs/QAMCm2eO0Z8B8ROMvAgyKaWIitkcvH8DwLfTrHMeXzCw==} + peerDependencies: + oxlint: ~1.73.0 + + eslint-plugin-perfectionist@5.10.0: + resolution: {integrity: sha512-HiqpDrUDbGrMC6iHQbemgDyHJ0366Vyz/qRWmxQcSAkmG25cXr8BdRgx8yAhOKhEfBXn8Rnf/mTCsV4EqUJSxg==} + engines: {node: ^20.0.0 || >=22.0.0} + peerDependencies: + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-dom@5.17.3: + resolution: {integrity: sha512-5SwntO1x0McFJ6taeNTu1RrWm7I7xBbeJdkdXE20pvEKmLif7p+XXqqm2StLX4UT/vEKPFXTZjqP0M8bWv0E5Q==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-jsx@5.17.3: + resolution: {integrity: sha512-PSeVABLiiBqYEJ6NE1nA+9Z1b2spmXLMpsMJdKMoTRzk5ACQ4coTGehxlkoFIOYe65fe1buXx85qGYarhO02iA==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + eslint-plugin-react-naming-convention@5.17.3: + resolution: {integrity: sha512-MuPbcGNXojHsbbC+bafoou84UbCMf01XrBbciRn3niHTzUgzkgt2yOPUj7SIGLdqjTu187RdoCuKX4yw4NhDEw==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + eslint-plugin-react-rsc@5.17.3: + resolution: {integrity: sha512-bwxus0dOO/S8PT9MoJ56sdtw1avewUUubjvp+prdOn0RMqs6C+fUFmeLoTldgGyTuHtx+CXvO3b2Izd3I/l4fA==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + eslint-plugin-react-web-api@5.17.3: + resolution: {integrity: sha512-czWJyYRNsLvDOaVVg/InZTBa+5I1j9gtErF+3+v7KX1uTqG4CggceLMc7wni5mNt0TzTHizVgKCWA+VPbBFYzw==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + eslint-plugin-react-x@5.17.3: + resolution: {integrity: sha512-iuKY6C6uHJokRYIRayrhYYAhgpHyEwRI4UTQ4+BLj/Ch8x/jkeMEhcZE5xud3lWG4QW0IGLgP+8RwZCLA6rYFw==} + engines: {node: '>=22.0.0'} + peerDependencies: + eslint: '*' + typescript: '*' + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.7.0: + resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -153,6 +1132,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.7.0: resolution: {integrity: sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==} engines: {node: '>= 16'} @@ -166,13 +1149,43 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.6: resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -181,9 +1194,18 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -192,10 +1214,22 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -204,10 +1238,19 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.13.5: resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==} engines: {node: '>=16.9.0'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -216,6 +1259,18 @@ packages: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.8: + resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -227,21 +1282,167 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jose@6.2.10: resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -262,13 +1463,33 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + negotiator@1.1.0: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -277,6 +1498,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -284,10 +1509,56 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + oxfmt@0.59.0: + resolution: {integrity: sha512-Xqk6cPZS1yMvVa7OAuenaDZUsgMDutvvbZ9/L5gSvAfW64+WN4HVhgipLj5rVERbYQt8fLs9TopyZ1rU1XEG/w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@0.25.0: + resolution: {integrity: sha512-7DBpqyLZCfyoXiivyfzt9Xmju/K1RcN+Y1W7buEwrgRCWWF11v9alypPqWGZBmh2erDkKL/kVyhKUH2Px+t13A==} + hasBin: true + + oxlint@1.73.0: + resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.24.0' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -295,14 +1566,36 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.16.0: resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} @@ -319,6 +1612,11 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -326,6 +1624,15 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -361,18 +1668,77 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-ts@2.3.1: + resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typescript-eslint@8.64.0: + resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -385,67 +1751,896 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: zod: ^3.25.28 || ^4 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} -snapshots: +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@7.2.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': + dependencies: + '@babel/traverse': 7.29.8(supports-color@7.2.0) + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8(supports-color@7.2.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@eslint-community/eslint-utils@4.10.1(eslint@10.7.0(supports-color@7.2.0))': + dependencies: + eslint: 10.7.0(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint-react/ast@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + string-ts: 2.3.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/core@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/var': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/eslint-plugin@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + eslint-plugin-react-dom: 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint-plugin-react-jsx: 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint-plugin-react-naming-convention: 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint-plugin-react-rsc: 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint-plugin-react-web-api: 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint-plugin-react-x: 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/eslint@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/jsx@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/var': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint-react/shared@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + ts-pattern: 5.9.0 + typescript: 5.9.3 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@eslint-react/var@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@eslint/config-array@0.23.5(supports-color@7.2.0)': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.7.0(supports-color@7.2.0))': + optionalDependencies: + eslint: 10.7.0(supports-color@7.2.0) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@hono/node-server@2.1.1(hono@4.13.5)': + dependencies: + hono: 4.13.5 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@3.25.76)': + dependencies: + '@hono/node-server': 2.1.1(hono@4.13.5) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + express: 5.2.1(supports-color@7.2.0) + express-rate-limit: 8.7.0(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0) + hono: 4.13.5 + jose: 6.2.10 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@oxc-project/types@0.147.0': {} + + '@oxfmt/binding-android-arm-eabi@0.59.0': + optional: true + + '@oxfmt/binding-android-arm64@0.59.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.59.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.59.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.59.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.59.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.59.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.59.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.59.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.59.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.59.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.25.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.25.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.25.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.25.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.25.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.25.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.73.0': + optional: true + + '@oxlint/binding-android-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-x64@1.73.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.73.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.73.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.73.0': + optional: true + + '@retn0/eslint-config-oxlint@0.1.2(eslint@10.7.0(supports-color@7.2.0))(oxlint@1.73.0(oxlint-tsgolint@0.25.0))': + dependencies: + eslint: 10.7.0(supports-color@7.2.0) + eslint-plugin-oxlint: 1.73.0(oxlint@1.73.0(oxlint-tsgolint@0.25.0)) + oxlint: 1.73.0(oxlint-tsgolint@0.25.0) + + '@retn0/eslint-config@2.1.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/eslint-plugin': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint/js': 10.0.1(eslint@10.7.0(supports-color@7.2.0)) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.7.0(supports-color@7.2.0)) + eslint: 10.7.0(supports-color@7.2.0) + eslint-plugin-perfectionist: 5.10.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint-plugin-react-hooks: 7.1.1(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + globals: 17.7.0 + typescript-eslint: 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@retn0/oxfmt-config@0.2.0(oxfmt@0.59.0)': + dependencies: + oxfmt: 0.59.0 + + '@retn0/oxlint-config@2.2.0(oxlint-tsgolint@0.25.0)(oxlint@1.73.0(oxlint-tsgolint@0.25.0))': + dependencies: + oxlint: 1.73.0(oxlint-tsgolint@0.25.0) + oxlint-tsgolint: 0.25.0 + + '@rolldown/binding-android-arm-eabi@1.2.6': + optional: true + + '@rolldown/binding-android-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-x64@1.2.6': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.6': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.6': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.6': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.6': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.6': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(supports-color@7.2.0))': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(supports-color@7.2.0)) + '@typescript-eslint/types': 8.69.0 + eslint: 10.7.0(supports-color@7.2.0) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.7 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.64.0 + eslint: 10.7.0(supports-color@7.2.0) + ignore: 7.0.8 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.64.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.69.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + + '@typescript-eslint/scope-manager@8.69.0': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + + '@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.69.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.64.0': {} + + '@typescript-eslint/types@8.69.0': {} + + '@typescript-eslint/typescript-estree@8.64.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.64.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.69.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + eslint-visitor-keys: 5.0.1 + + '@typescript-eslint/visitor-keys@8.69.0': + dependencies: + '@typescript-eslint/types': 8.69.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.11 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.4 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)) + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@24.13.3))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@24.13.3) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 - '@hono/node-server@2.1.1(hono@4.13.5)': + '@vitest/runner@4.1.11': dependencies: - hono: 4.13.5 + '@vitest/utils': 4.1.11 + pathe: 2.0.3 - '@modelcontextprotocol/sdk@1.30.0(zod@3.25.76)': + '@vitest/snapshot@4.1.11': dependencies: - '@hono/node-server': 2.1.1(hono@4.13.5) - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.1.1 - express: 5.2.1 - express-rate-limit: 8.7.0(express@5.2.1) - hono: 4.13.5 - jose: 6.2.10 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - transitivePeerDependencies: - - supports-color + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 - '@types/node@24.13.3': + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': dependencies: - undici-types: 7.18.2 + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.1.0 + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -453,11 +2648,25 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - body-parser@2.3.0: + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.20: {} + + birecord@0.1.2: {} + + body-parser@2.3.0(supports-color@7.2.0): dependencies: bytes: 3.1.2 content-type: 2.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 @@ -467,6 +2676,18 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.420 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -479,12 +2700,20 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001810: {} + + chai@6.2.2: {} + + compare-versions@6.1.1: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} content-type@2.1.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -500,12 +2729,18 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + deep-is@0.1.4: {} depd@2.0.0: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -514,18 +2749,221 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.420: {} + encodeurl@2.0.0: {} es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 + escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + + eslint-plugin-oxlint@1.73.0(oxlint@1.73.0(oxlint-tsgolint@0.25.0)): + dependencies: + jsonc-parser: 3.3.1 + oxlint: 1.73.0(oxlint-tsgolint@0.25.0) + + eslint-plugin-perfectionist@5.10.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-react-dom@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/jsx': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks@7.1.1(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/parser': 7.29.8 + eslint: 10.7.0(supports-color@7.2.0) + hermes-parser: 0.25.1 + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-jsx@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/core': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/jsx': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-naming-convention@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/core': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/var': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-rsc@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/core': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/var': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-web-api@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/core': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/var': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + birecord: 0.1.2 + eslint: 10.7.0(supports-color@7.2.0) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-x@5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/core': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/eslint': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/jsx': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/shared': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@eslint-react/var': 5.17.3(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/type-utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 10.7.0(supports-color@7.2.0) + string-ts: 2.3.1 + ts-api-utils: 2.5.0(typescript@5.9.3) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.7.0(supports-color@7.2.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.7.0(supports-color@7.2.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5(supports-color@7.2.0) + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + etag@1.8.1: {} eventsource-parser@3.1.1: {} @@ -534,28 +2972,30 @@ snapshots: dependencies: eventsource-parser: 3.1.1 - express-rate-limit@8.7.0(express@5.2.1): + expect-type@1.4.0: {} + + express-rate-limit@8.7.0(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - debug: 4.4.3 - express: 5.2.1 + debug: 4.4.3(supports-color@7.2.0) + express: 5.2.1(supports-color@7.2.0) ip-address: 10.7.0 transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@7.2.0): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@7.2.0) content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@7.2.0) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -566,9 +3006,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.16.0 range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 + router: 2.2.0(supports-color@7.2.0) + send: 1.2.1(supports-color@7.2.0) + serve-static: 2.2.1(supports-color@7.2.0) statuses: 2.0.2 type-is: 2.1.0 vary: 1.1.2 @@ -577,11 +3017,23 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fast-uri@3.1.6: {} - finalhandler@2.1.1: + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + finalhandler@2.1.1(supports-color@7.2.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -590,12 +3042,29 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + forwarded@0.2.0: {} fresh@2.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -614,16 +3083,32 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.7.0: {} + gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} hasown@2.0.4: dependencies: function-bind: 1.1.2 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hono@4.13.5: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -636,22 +3121,143 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ignore@5.3.2: {} + + ignore@7.0.8: {} + + imurmurhash@0.1.4: {} + inherits@2.0.4: {} ip-address@10.7.0: {} ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-promise@4.0.0: {} isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jose@6.2.10: {} + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.4: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + math-intrinsics@1.1.0: {} media-typer@1.1.1: {} @@ -664,16 +3270,30 @@ snapshots: dependencies: mime-db: 1.54.0 + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + ms@2.1.3: {} + nanoid@3.3.18: {} + + natural-compare@1.4.0: {} + + natural-orderby@5.0.0: {} + negotiator@1.1.0: dependencies: content-type: 2.1.0 + node-releases@2.0.54: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} + obug@2.1.4: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -682,19 +3302,110 @@ snapshots: dependencies: wrappy: 1.0.2 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + oxfmt@0.59.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.59.0 + '@oxfmt/binding-android-arm64': 0.59.0 + '@oxfmt/binding-darwin-arm64': 0.59.0 + '@oxfmt/binding-darwin-x64': 0.59.0 + '@oxfmt/binding-freebsd-x64': 0.59.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.59.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.59.0 + '@oxfmt/binding-linux-arm64-gnu': 0.59.0 + '@oxfmt/binding-linux-arm64-musl': 0.59.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.59.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.59.0 + '@oxfmt/binding-linux-riscv64-musl': 0.59.0 + '@oxfmt/binding-linux-s390x-gnu': 0.59.0 + '@oxfmt/binding-linux-x64-gnu': 0.59.0 + '@oxfmt/binding-linux-x64-musl': 0.59.0 + '@oxfmt/binding-openharmony-arm64': 0.59.0 + '@oxfmt/binding-win32-arm64-msvc': 0.59.0 + '@oxfmt/binding-win32-ia32-msvc': 0.59.0 + '@oxfmt/binding-win32-x64-msvc': 0.59.0 + + oxlint-tsgolint@0.25.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.25.0 + '@oxlint-tsgolint/darwin-x64': 0.25.0 + '@oxlint-tsgolint/linux-arm64': 0.25.0 + '@oxlint-tsgolint/linux-x64': 0.25.0 + '@oxlint-tsgolint/win32-arm64': 0.25.0 + '@oxlint-tsgolint/win32-x64': 0.25.0 + + oxlint@1.73.0(oxlint-tsgolint@0.25.0): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.73.0 + '@oxlint/binding-android-arm64': 1.73.0 + '@oxlint/binding-darwin-arm64': 1.73.0 + '@oxlint/binding-darwin-x64': 1.73.0 + '@oxlint/binding-freebsd-x64': 1.73.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.73.0 + '@oxlint/binding-linux-arm-musleabihf': 1.73.0 + '@oxlint/binding-linux-arm64-gnu': 1.73.0 + '@oxlint/binding-linux-arm64-musl': 1.73.0 + '@oxlint/binding-linux-ppc64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-musl': 1.73.0 + '@oxlint/binding-linux-s390x-gnu': 1.73.0 + '@oxlint/binding-linux-x64-gnu': 1.73.0 + '@oxlint/binding-linux-x64-musl': 1.73.0 + '@oxlint/binding-openharmony-arm64': 1.73.0 + '@oxlint/binding-win32-arm64-msvc': 1.73.0 + '@oxlint/binding-win32-ia32-msvc': 1.73.0 + '@oxlint/binding-win32-x64-msvc': 1.73.0 + oxlint-tsgolint: 0.25.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + parseurl@1.3.3: {} + path-exists@4.0.0: {} + path-key@3.1.1: {} path-to-regexp@8.4.2: {} + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + pkce-challenge@5.0.1: {} + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.16.0: dependencies: es-define-property: 1.0.1 @@ -711,9 +3422,30 @@ snapshots: require-from-string@2.0.2: {} - router@2.2.0: + rolldown@1.2.6: + dependencies: + '@oxc-project/types': 0.147.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + + router@2.2.0(supports-color@7.2.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -723,9 +3455,13 @@ snapshots: safer-buffer@2.1.2: {} - send@1.2.1: + semver@6.3.1: {} + + semver@7.8.5: {} + + send@1.2.1(supports-color@7.2.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -739,12 +3475,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1: + serve-static@2.2.1(supports-color@7.2.0): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -784,32 +3520,144 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.2.0: {} + + string-ts@2.3.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@2.1.0: {} + + tinyrainbow@3.1.1: {} + toidentifier@1.0.1: {} + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-pattern@5.9.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-is@2.1.0: dependencies: content-type: 2.1.0 media-typer: 1.1.1 mime-types: 3.0.2 + typescript-eslint@8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 10.7.0(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} undici-types@7.18.2: {} unpipe@1.0.0: {} + update-browserslist-db@1.3.2(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + vary@1.1.2: {} + vite@8.2.2(@types/node@24.13.3): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.6 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + + vitest@4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@24.13.3)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@24.13.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) + transitivePeerDependencies: + - msw + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + wrappy@1.0.2: {} + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 + zod-validation-error@4.0.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} diff --git a/scripts/clean-dist.mjs b/scripts/clean-dist.mjs index 801a73a..56718d3 100644 --- a/scripts/clean-dist.mjs +++ b/scripts/clean-dist.mjs @@ -1,10 +1,10 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const dist = path.join(projectRoot, "dist"); -if (path.dirname(dist) !== projectRoot || path.basename(dist) !== "dist") { +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const dist = path.join(projectRoot, 'dist'); +if (path.dirname(dist) !== projectRoot || path.basename(dist) !== 'dist') { throw new Error(`Refusing to clean unexpected path: ${dist}`); } fs.rmSync(dist, { recursive: true, force: true }); diff --git a/src/auth/provider.ts b/src/auth/provider.ts index 14c4568..6731435 100644 --- a/src/auth/provider.ts +++ b/src/auth/provider.ts @@ -1,8 +1,8 @@ -import type { IncomingHttpHeaders } from "node:http"; +import type { IncomingHttpHeaders } from 'node:http'; export interface Principal { readonly id: string; - readonly authenticationMethod: "single-user" | "oauth"; + readonly authenticationMethod: 'single-user' | 'oauth'; } export interface AuthProvider { diff --git a/src/auth/single-user-provider.ts b/src/auth/single-user-provider.ts index 4a8c0d4..46a08dd 100644 --- a/src/auth/single-user-provider.ts +++ b/src/auth/single-user-provider.ts @@ -1,12 +1,12 @@ -import type { AuthProvider, Principal } from "./provider.js"; +import type { AuthProvider, Principal } from './provider.js'; const localPrincipal: Principal = { - id: "local-owner", - authenticationMethod: "single-user", + id: 'local-owner', + authenticationMethod: 'single-user', }; export class SingleUserAuthProvider implements AuthProvider { - async authenticate(): Promise { - return localPrincipal; + authenticate(): Promise { + return Promise.resolve(localPrincipal); } } diff --git a/src/cli/main.ts b/src/cli/main.ts index f4ea3b1..457b3f8 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -1,6 +1,6 @@ -import { loadAppConfig } from "../config.js"; -import { StateDatabase } from "../state/database.js"; -import { WorkspaceService } from "../workspaces/service.js"; +import { loadAppConfig } from '../config.js'; +import { StateDatabase } from '../state/database.js'; +import { WorkspaceService } from '../workspaces/service.js'; const config = loadAppConfig(); const database = new StateDatabase(config.databasePath); @@ -11,28 +11,32 @@ const workspaces = new WorkspaceService({ allowedHostRoots: config.allowedHostRoots, }); const [group, action, value, ...rest] = process.argv.slice(2); -const ownerId = "local-owner"; +const ownerId = 'local-owner'; -function modeFrom(args: readonly string[]): "clone" | "direct" { - const index = args.indexOf("--mode"); - const mode = index >= 0 ? args[index + 1] : "clone"; - if (mode !== "clone" && mode !== "direct") throw new Error("--mode must be clone or direct"); +function modeFrom(args: readonly string[]): 'clone' | 'direct' { + const index = args.indexOf('--mode'); + const mode = index >= 0 ? args[index + 1] : 'clone'; + if (mode !== 'clone' && mode !== 'direct') { + throw new Error('--mode must be clone or direct'); + } return mode; } try { - if (group === "workspace" && action === "list") { + if (group === 'workspace' && action === 'list') { console.log(JSON.stringify(workspaces.list(ownerId), null, 2)); - } else if (group === "workspace" && action === "add" && value) { + } else if (group === 'workspace' && action === 'add' && value) { console.log(JSON.stringify(workspaces.registerHost(ownerId, value, modeFrom(rest)), null, 2)); - } else if (group === "approval" && action === "list") { + } else if (group === 'approval' && action === 'list') { console.log(JSON.stringify(database.listApprovals(), null, 2)); - } else if (group === "approval" && action === "approve" && value) { + } else if (group === 'approval' && action === 'approve' && value) { console.log(JSON.stringify(workspaces.approve(value), null, 2)); - } else if (group === "approval" && action === "reject" && value) { + } else if (group === 'approval' && action === 'reject' && value) { console.log(JSON.stringify(workspaces.reject(value), null, 2)); } else { - console.error("Usage:\n chat2shell workspace list\n chat2shell workspace add PATH [--mode clone|direct]\n chat2shell approval list\n chat2shell approval approve ID\n chat2shell approval reject ID"); + console.error( + 'Usage:\n chat2shell workspace list\n chat2shell workspace add PATH [--mode clone|direct]\n chat2shell approval list\n chat2shell approval approve ID\n chat2shell approval reject ID', + ); process.exitCode = 2; } } finally { diff --git a/src/codexpro/client-pool.ts b/src/codexpro/client-pool.ts index 20654aa..d7d472b 100644 --- a/src/codexpro/client-pool.ts +++ b/src/codexpro/client-pool.ts @@ -1,6 +1,6 @@ -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import type { Sandbox } from "../domain/types.js"; -import type { SandboxService } from "../sandbox/service.js"; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { Sandbox } from '../domain/types.js'; +import type { SandboxService } from '../sandbox/service.js'; interface JsonRpcResponse { readonly id?: unknown; @@ -8,36 +8,65 @@ interface JsonRpcResponse { readonly error?: { readonly code: number; readonly message: string; readonly data?: unknown }; } -export function normalizeWorkspaceIdentity(result: CallToolResult, workspaceId: string): CallToolResult { +export function normalizeWorkspaceIdentity( + result: CallToolResult, + workspaceId: string, +): CallToolResult { const structuredContent = result.structuredContent; - if (!structuredContent) return result; + if (!structuredContent) { + return result; + } + + const internalIds = [ + structuredContent.workspace_id, + structuredContent.selected_workspace_id, + ].filter((value): value is string => typeof value === 'string' && value !== workspaceId); + if (internalIds.length === 0) { + return result; + } - const internalIds = [structuredContent.workspace_id, structuredContent.selected_workspace_id] - .filter((value): value is string => typeof value === "string" && value !== workspaceId); - if (internalIds.length === 0) return result; + const publicText = (text: string): string => + internalIds.reduce( + (normalized, internalId) => normalized.replaceAll(internalId, workspaceId), + text, + ); - const publicText = (text: string): string => internalIds.reduce( - (normalized, internalId) => normalized.replaceAll(internalId, workspaceId), - text, - ); return { ...result, - content: result.content.map((item) => item.type === "text" ? { ...item, text: publicText(item.text) } : item), + content: result.content.map((item) => + item.type === 'text' ? { ...item, text: publicText(item.text) } : item, + ), structuredContent: { ...structuredContent, - ...(typeof structuredContent.workspace_id === "string" ? { workspace_id: workspaceId } : {}), - ...(typeof structuredContent.selected_workspace_id === "string" ? { selected_workspace_id: workspaceId } : {}), + ...(typeof structuredContent.workspace_id === 'string' ? { workspace_id: workspaceId } : {}), + ...(typeof structuredContent.selected_workspace_id === 'string' + ? { selected_workspace_id: workspaceId } + : {}), }, }; } function parseEventStream(text: string, expectedId: number): JsonRpcResponse { for (const line of text.split(/\r?\n/)) { - if (!line.startsWith("data:")) continue; + if (!line.startsWith('data:')) { + continue; + } const message = JSON.parse(line.slice(5).trim()) as JsonRpcResponse; - if (message.id === expectedId) return message; + if (message.id === expectedId) { + return message; + } } - throw new Error("CodexPro returned an event stream without the expected response"); + throw new Error('CodexPro returned an event stream without the expected response'); +} + +function toolResult(response: JsonRpcResponse): CallToolResult { + if (response.error) { + throw new Error(`CodexPro ${response.error.code}: ${response.error.message}`); + } + if (!response.result) { + throw new Error('CodexPro returned no tool result'); + } + return response.result; } class CodexProSession { @@ -47,7 +76,9 @@ class CodexProSession { #nextId = 1; constructor(sandbox: Sandbox) { - if (!sandbox.endpoint || !sandbox.authToken) throw new Error("Sandbox is missing its CodexPro connection"); + if (!sandbox.endpoint || !sandbox.authToken) { + throw new Error('Sandbox is missing its CodexPro connection'); + } this.#endpoint = sandbox.endpoint; this.#token = sandbox.authToken; } @@ -57,27 +88,27 @@ class CodexProSession { } async callTool(name: string, args: Record): Promise { - if (!this.#sessionId) await this.#initialize(); + if (!this.#sessionId) { + await this.#initialize(); + } try { - const response = await this.#request("tools/call", { name, arguments: args }); - if (response.error) throw new Error(`CodexPro ${response.error.code}: ${response.error.message}`); - if (!response.result) throw new Error("CodexPro returned no tool result"); - return response.result; + return toolResult(await this.#request('tools/call', { name, arguments: args })); } catch (error) { - if (!(error instanceof Error) || !/HTTP 404/.test(error.message)) throw error; + if (!(error instanceof Error) || !/HTTP 404/.test(error.message)) { + throw error; + } this.#sessionId = undefined; await this.#initialize(); - const response = await this.#request("tools/call", { name, arguments: args }); - if (response.error) throw new Error(`CodexPro ${response.error.code}: ${response.error.message}`); - if (!response.result) throw new Error("CodexPro returned no tool result"); - return response.result; + return toolResult(await this.#request('tools/call', { name, arguments: args })); } } async close(): Promise { - if (!this.#sessionId) return; + if (!this.#sessionId) { + return; + } await fetch(this.#endpoint, { - method: "DELETE", + method: 'DELETE', headers: this.#headers(), signal: AbortSignal.timeout(2_000), }).catch(() => undefined); @@ -85,45 +116,63 @@ class CodexProSession { } async #initialize(): Promise { - const response = await this.#request("initialize", { - protocolVersion: "2025-06-18", - capabilities: {}, - clientInfo: { name: "chat2shell", version: "0.2.0" }, - }, false); - if (response.error) throw new Error(`CodexPro initialize failed: ${response.error.message}`); - if (!this.#sessionId) throw new Error("CodexPro initialize response did not include a session id"); + const response = await this.#request( + 'initialize', + { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { name: 'chat2shell', version: '0.2.0' }, + }, + false, + ); + if (response.error) { + throw new Error(`CodexPro initialize failed: ${response.error.message}`); + } + if (!this.#sessionId) { + throw new Error('CodexPro initialize response did not include a session id'); + } await fetch(this.#endpoint, { - method: "POST", + method: 'POST', headers: this.#headers(), - body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), + body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }), signal: AbortSignal.timeout(5_000), }); } - async #request(method: string, params: Record, includeSession = true): Promise { + async #request( + method: string, + params: Record, + includeSession = true, + ): Promise { const id = this.#nextId++; const response = await fetch(this.#endpoint, { - method: "POST", + method: 'POST', headers: this.#headers(includeSession), - body: JSON.stringify({ jsonrpc: "2.0", id, method, params }), + body: JSON.stringify({ jsonrpc: '2.0', id, method, params }), signal: AbortSignal.timeout(610_000), }); - if (!response.ok) throw new Error(`CodexPro HTTP ${response.status}: ${await response.text()}`); - const sessionId = response.headers.get("mcp-session-id"); - if (sessionId) this.#sessionId = sessionId; + if (!response.ok) { + throw new Error(`CodexPro HTTP ${response.status}: ${await response.text()}`); + } + const sessionId = response.headers.get('mcp-session-id'); + if (sessionId) { + this.#sessionId = sessionId; + } const text = await response.text(); - return response.headers.get("content-type")?.includes("text/event-stream") + return response.headers.get('content-type')?.includes('text/event-stream') ? parseEventStream(text, id) - : JSON.parse(text) as JsonRpcResponse; + : (JSON.parse(text) as JsonRpcResponse); } #headers(includeSession = true): Record { const headers: Record = { - accept: "application/json, text/event-stream", - authorization: `Bearer ${this.#token}`, - "content-type": "application/json", + 'accept': 'application/json, text/event-stream', + 'authorization': `Bearer ${this.#token}`, + 'content-type': 'application/json', }; - if (includeSession && this.#sessionId) headers["mcp-session-id"] = this.#sessionId; + if (includeSession && this.#sessionId) { + headers['mcp-session-id'] = this.#sessionId; + } return headers; } } @@ -137,7 +186,12 @@ export class CodexProClientPool { sandboxes.onDestroy((sandboxId) => this.close(sandboxId)); } - async call(ownerId: string, sandboxId: string, toolName: string, args: Record): Promise { + async call( + ownerId: string, + sandboxId: string, + toolName: string, + args: Record, + ): Promise { return this.#sandboxes.withReady(ownerId, sandboxId, async (sandbox) => { let session = this.#sessions.get(sandbox.id); if (!session?.matches(sandbox)) { @@ -145,7 +199,10 @@ export class CodexProClientPool { session = new CodexProSession(sandbox); this.#sessions.set(sandbox.id, session); } - return normalizeWorkspaceIdentity(await session.callTool(toolName, args), sandbox.workspaceId); + return normalizeWorkspaceIdentity( + await session.callTool(toolName, args), + sandbox.workspaceId, + ); }); } diff --git a/src/codexpro/tool-manifest.ts b/src/codexpro/tool-manifest.ts index 507fe1f..9b2d6e1 100644 --- a/src/codexpro/tool-manifest.ts +++ b/src/codexpro/tool-manifest.ts @@ -1,5 +1,5 @@ -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; -import standardTools from "./standard-tools.json" with { type: "json" }; +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +import standardTools from './standard-tools.json' with { type: 'json' }; export function codexProToolManifest(): readonly Tool[] { return standardTools as unknown as readonly Tool[]; @@ -7,7 +7,9 @@ export function codexProToolManifest(): readonly Tool[] { export function scopedCodexProTool(tool: Tool): Tool { const properties = tool.inputSchema.properties ?? {}; - if ("sandbox_id" in properties) throw new Error(`CodexPro tool conflicts with the chat2shell routing field: ${tool.name}`); + if ('sandbox_id' in properties) { + throw new Error(`CodexPro tool conflicts with the chat2shell routing field: ${tool.name}`); + } return { ...tool, description: `${tool.description ?? tool.name} Runs only inside the selected chat2shell sandbox.`, @@ -15,12 +17,12 @@ export function scopedCodexProTool(tool: Tool): Tool { ...tool.inputSchema, properties: { sandbox_id: { - type: "string", - description: "Sandbox id from sandbox_create or sandbox_list.", + type: 'string', + description: 'Sandbox id from sandbox_create or sandbox_list.', }, ...properties, }, - required: [...new Set(["sandbox_id", ...(tool.inputSchema.required ?? [])])], + required: [...new Set(['sandbox_id', ...(tool.inputSchema.required ?? [])])], additionalProperties: false, }, }; diff --git a/src/config.ts b/src/config.ts index 3a513bc..d4169e4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ -import os from "node:os"; -import path from "node:path"; +import os from 'node:os'; +import path from 'node:path'; export interface AppConfig { readonly host: string; @@ -20,13 +20,17 @@ export interface AppConfig { function readPort(value: string | undefined, fallback: number, name: string): number { const port = Number(value ?? fallback); - if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) throw new Error(`${name} must be an integer from 1 to 65535`); + if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) { + throw new Error(`${name} must be an integer from 1 to 65535`); + } return port; } function expandHome(value: string): string { - if (value === "~") return os.homedir(); - return value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value; + if (value === '~') { + return os.homedir(); + } + return value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value; } function resolvePath(value: string): string { @@ -34,26 +38,30 @@ function resolvePath(value: string): string { } export function loadAppConfig(environment: NodeJS.ProcessEnv = process.env): AppConfig { - const dataRoot = resolvePath(environment.CHAT2SHELL_DATA_ROOT ?? "~/.chat2shell"); - const stateDir = resolvePath(environment.CHAT2SHELL_STATE_DIR ?? path.join(dataRoot, "state")); - const workspaceRoot = resolvePath(environment.CHAT2SHELL_WORKSPACE_ROOT ?? path.join(dataRoot, "workspaces")); - const defaultAllowedRoot = path.join(os.homedir(), "repositories"); + const dataRoot = resolvePath(environment.CHAT2SHELL_DATA_ROOT ?? '~/.chat2shell'); + const stateDir = resolvePath(environment.CHAT2SHELL_STATE_DIR ?? path.join(dataRoot, 'state')); + const workspaceRoot = resolvePath( + environment.CHAT2SHELL_WORKSPACE_ROOT ?? path.join(dataRoot, 'workspaces'), + ); + const defaultAllowedRoot = path.join(os.homedir(), 'repositories'); const allowedHostRoots = (environment.CHAT2SHELL_ALLOWED_HOST_ROOTS ?? defaultAllowedRoot) .split(path.delimiter) .filter(Boolean) .map(resolvePath); return { - host: environment.CHAT2SHELL_HOST ?? "127.0.0.1", - port: readPort(environment.CHAT2SHELL_PORT, 18_788, "CHAT2SHELL_PORT"), + host: environment.CHAT2SHELL_HOST ?? '127.0.0.1', + port: readPort(environment.CHAT2SHELL_PORT, 18_788, 'CHAT2SHELL_PORT'), maxBodyBytes: 20 * 1024 * 1024, dataRoot, workspaceRoot, stateDir, - databasePath: resolvePath(environment.CHAT2SHELL_DATABASE_PATH ?? path.join(stateDir, "chat2shell.sqlite")), + databasePath: resolvePath( + environment.CHAT2SHELL_DATABASE_PATH ?? path.join(stateDir, 'chat2shell.sqlite'), + ), allowedHostRoots, - sbxBinary: "sbx", - sandboxTemplate: "chat2shell-codexpro:0.30.0", + sbxBinary: 'sbx', + sandboxTemplate: 'chat2shell-codexpro:0.30.0', sandboxPort: 18_787, idleTimeoutMs: 24 * 60 * 60_000, workspaceRetentionMs: 30 * 24 * 60 * 60_000, diff --git a/src/domain/ids.ts b/src/domain/ids.ts index 6ee5b21..c97af9c 100644 --- a/src/domain/ids.ts +++ b/src/domain/ids.ts @@ -1,9 +1,9 @@ -import { randomBytes } from "node:crypto"; +import { randomBytes } from 'node:crypto'; -export type IdPrefix = "ws" | "sbx" | "approval"; +export type IdPrefix = 'ws' | 'sbx' | 'approval'; export function createId(prefix: IdPrefix): string { - const timestamp = Date.now().toString(36).padStart(9, "0"); - const random = randomBytes(8).toString("hex"); + const timestamp = Date.now().toString(36).padStart(9, '0'); + const random = randomBytes(8).toString('hex'); return `${prefix}_${timestamp}${random}`; } diff --git a/src/domain/types.ts b/src/domain/types.ts index 4b67359..4f1ea96 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -1,8 +1,8 @@ -export type WorkspaceKind = "managed" | "host"; -export type WorkspaceMode = "managed" | "clone" | "direct"; -export type WorkspaceStatus = "approved" | "retained" | "trashed"; -export type ApprovalStatus = "pending" | "approved" | "rejected"; -export type SandboxStatus = "creating" | "running" | "destroying" | "destroyed" | "failed"; +export type WorkspaceKind = 'managed' | 'host'; +export type WorkspaceMode = 'managed' | 'clone' | 'direct'; +export type WorkspaceStatus = 'approved' | 'retained' | 'trashed'; +export type ApprovalStatus = 'pending' | 'approved' | 'rejected'; +export type SandboxStatus = 'creating' | 'running' | 'destroying' | 'destroyed' | 'failed'; export interface Workspace { readonly id: string; @@ -19,7 +19,7 @@ export interface Approval { readonly id: string; readonly ownerId: string; readonly requestedPath: string; - readonly mode: Exclude; + readonly mode: Exclude; readonly status: ApprovalStatus; readonly workspaceId?: string; readonly createdAt: number; @@ -54,7 +54,7 @@ export interface SandboxSummary { } export interface SandboxCreateResult { - readonly status: "created" | "reused" | "approval_required"; + readonly status: 'created' | 'reused' | 'approval_required'; readonly sandbox?: SandboxSummary; readonly approval?: Approval; } diff --git a/src/mcp/compatibility.ts b/src/mcp/compatibility.ts index e334414..a5d05dc 100644 --- a/src/mcp/compatibility.ts +++ b/src/mcp/compatibility.ts @@ -1,4 +1,4 @@ -import type { IncomingHttpHeaders } from "node:http"; +import type { IncomingHttpHeaders } from 'node:http'; interface JsonRpcRequest { readonly id?: unknown; @@ -6,7 +6,7 @@ interface JsonRpcRequest { } export interface JsonRpcErrorResponse { - readonly jsonrpc: "2.0"; + readonly jsonrpc: '2.0'; readonly id: unknown; readonly error: { readonly code: number; @@ -18,20 +18,20 @@ export function sessionlessDiscoverResponse( headers: IncomingHttpHeaders, body: Buffer, ): JsonRpcErrorResponse | undefined { - if (typeof headers["mcp-session-id"] === "string") { + if (typeof headers['mcp-session-id'] === 'string') { return undefined; } try { - const request = JSON.parse(body.toString("utf8")) as JsonRpcRequest; - if (request.method !== "server/discover") { + const request = JSON.parse(body.toString('utf8')) as JsonRpcRequest; + if (request.method !== 'server/discover') { return undefined; } return { - jsonrpc: "2.0", + jsonrpc: '2.0', id: request.id ?? null, - error: { code: -32601, message: "Method not found" }, + error: { code: -32601, message: 'Method not found' }, }; } catch { return undefined; diff --git a/src/mcp/control-server.ts b/src/mcp/control-server.ts index 61f664e..779f38b 100644 --- a/src/mcp/control-server.ts +++ b/src/mcp/control-server.ts @@ -1,172 +1,254 @@ -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult, type Tool } from "@modelcontextprotocol/sdk/types.js"; -import type { CodexProClientPool } from "../codexpro/client-pool.js"; -import { scopedCodexProTool } from "../codexpro/tool-manifest.js"; -import type { SandboxService } from "../sandbox/service.js"; -import type { WorkspaceService } from "../workspaces/service.js"; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + CallToolRequestSchema, + type CallToolResult, + ListToolsRequestSchema, + type Tool, +} from '@modelcontextprotocol/sdk/types.js'; +import type { CodexProClientPool } from '../codexpro/client-pool.js'; +import type { SandboxService } from '../sandbox/service.js'; +import type { WorkspaceService } from '../workspaces/service.js'; +import { scopedCodexProTool } from '../codexpro/tool-manifest.js'; const sandboxCreateTool: Tool = { - name: "sandbox_create", - title: "Create or Reuse Sandbox", - description: "Create an isolated Docker Sandbox, reuse the active sandbox for a workspace, or request host approval for a new host path.", + name: 'sandbox_create', + title: 'Create or Reuse Sandbox', + description: + 'Create an isolated Docker Sandbox, reuse the active sandbox for a workspace, or request host approval for a new host path.', inputSchema: { - type: "object", + type: 'object', properties: { - workspace_id: { type: "string", description: "Approved persistent workspace id. Omit with workspace_path to create a managed workspace." }, - workspace_path: { type: "string", description: "Host path to request. It is never mounted until approved locally." }, - workspace_mode: { type: "string", enum: ["managed", "clone", "direct"], description: "Defaults to managed without a path and clone with a host path." }, + workspace_id: { + type: 'string', + description: + 'Approved persistent workspace id. Omit with workspace_path to create a managed workspace.', + }, + workspace_path: { + type: 'string', + description: 'Host path to request. It is never mounted until approved locally.', + }, + workspace_mode: { + type: 'string', + enum: ['managed', 'clone', 'direct'], + description: 'Defaults to managed without a path and clone with a host path.', + }, }, additionalProperties: false, }, - annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: false }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: false, + idempotentHint: false, + }, }; const sandboxListTool: Tool = { - name: "sandbox_list", - title: "List Sandboxes", - description: "List running and failed sandboxes owned by the current chat2shell principal. Running IDs can be reused from other conversations; failed sandboxes must be destroyed.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, + name: 'sandbox_list', + title: 'List Sandboxes', + description: + 'List running and failed sandboxes owned by the current chat2shell principal. Running IDs can be reused from other conversations; failed sandboxes must be destroyed.', + inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, }; const sandboxGetTool: Tool = { - name: "sandbox_get", - title: "Get Sandbox", - description: "Get the current state, workspace, and expiration times for one sandbox.", + name: 'sandbox_get', + title: 'Get Sandbox', + description: 'Get the current state, workspace, and expiration times for one sandbox.', inputSchema: { - type: "object", - properties: { sandbox_id: { type: "string" } }, - required: ["sandbox_id"], + type: 'object', + properties: { sandbox_id: { type: 'string' } }, + required: ['sandbox_id'], additionalProperties: false, }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, }; const sandboxDestroyTool: Tool = { - name: "sandbox_destroy", - title: "Destroy Sandbox", - description: "Permanently remove one sandbox microVM. Managed workspace files are retained for 30 days; registered host workspaces are never deleted.", + name: 'sandbox_destroy', + title: 'Destroy Sandbox', + description: + 'Permanently remove one sandbox microVM. Managed workspace files are retained for 30 days; registered host workspaces are never deleted.', inputSchema: { - type: "object", - properties: { sandbox_id: { type: "string" } }, - required: ["sandbox_id"], + type: 'object', + properties: { sandbox_id: { type: 'string' } }, + required: ['sandbox_id'], additionalProperties: false, }, - annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, idempotentHint: true }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: false, + idempotentHint: true, + }, }; const sandboxExposeTool: Tool = { - name: "sandbox_expose", - title: "Expose Sandbox Port", - description: "Publish one TCP port from a running sandbox on an automatically assigned port on every host IPv4 interface. The service inside the sandbox must listen on 0.0.0.0. The mapping has no separate authentication or expiration and disappears with the sandbox. Traffic through it does not renew sandbox activity.", + name: 'sandbox_expose', + title: 'Expose Sandbox Port', + description: + 'Publish one TCP port from a running sandbox on an automatically assigned port on every host IPv4 interface. The service inside the sandbox must listen on 0.0.0.0. The mapping has no separate authentication or expiration and disappears with the sandbox. Traffic through it does not renew sandbox activity.', inputSchema: { - type: "object", + type: 'object', properties: { - sandbox_id: { type: "string" }, - port: { type: "integer", minimum: 1, maximum: 65_535 }, + sandbox_id: { type: 'string' }, + port: { type: 'integer', minimum: 1, maximum: 65_535 }, }, - required: ["sandbox_id", "port"], + required: ['sandbox_id', 'port'], additionalProperties: false, }, - annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true, idempotentHint: true }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + idempotentHint: true, + }, }; const workspaceListTool: Tool = { - name: "workspace_list", - title: "List Workspaces", - description: "List managed and locally approved workspaces available to the current principal.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, + name: 'workspace_list', + title: 'List Workspaces', + description: 'List managed and locally approved workspaces available to the current principal.', + inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, }; -const managementTools = [sandboxCreateTool, sandboxListTool, sandboxGetTool, sandboxExposeTool, sandboxDestroyTool, workspaceListTool] as const; +const managementTools = [ + sandboxCreateTool, + sandboxListTool, + sandboxGetTool, + sandboxExposeTool, + sandboxDestroyTool, + workspaceListTool, +] as const; function objectArgs(value: unknown): Record { - if (value === undefined) return {}; - if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Tool arguments must be an object"); + if (value === undefined) { + return {}; + } + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Tool arguments must be an object'); + } return value as Record; } function optionalString(args: Record, name: string): string | undefined { const value = args[name]; - if (value === undefined) return undefined; - if (typeof value !== "string" || value.length === 0) throw new Error(`${name} must be a non-empty string`); + if (value === undefined) { + return undefined; + } + if (typeof value !== 'string' || value.length === 0) { + throw new Error(`${name} must be a non-empty string`); + } return value; } function requiredNumber(args: Record, name: string): number { const value = args[name]; - if (typeof value !== "number") throw new Error(`${name} must be a number`); + if (typeof value !== 'number') { + throw new Error(`${name} must be a number`); + } return value; } function jsonResult(value: unknown): CallToolResult { return { - content: [{ type: "text", text: JSON.stringify(value, null, 2) }], + content: [{ type: 'text', text: JSON.stringify(value, null, 2) }], structuredContent: value as Record, }; } function errorResult(error: unknown): CallToolResult { const message = error instanceof Error ? error.message : String(error); - return { isError: true, content: [{ type: "text", text: message }] }; + return { isError: true, content: [{ type: 'text', text: message }] }; } export interface ControlServerDependencies { readonly principalId: string; - readonly sandboxes: Pick; - readonly workspaces: Pick; - readonly codexPro: Pick; + readonly sandboxes: Pick; + readonly workspaces: Pick; + readonly codexPro: Pick; readonly codexProTools: readonly Tool[]; } export function createControlServer(dependencies: ControlServerDependencies): Server { const server = new Server( - { name: "chat2shell", version: "0.2.0" }, + { name: 'chat2shell', version: '0.2.0' }, { capabilities: { tools: {} }, - instructions: "Create or select an isolated sandbox first. Every CodexPro tool requires an explicit sandbox_id. Bash is unrestricted inside the sandbox but never has host shell or host Docker access.", + instructions: + 'Create or select an isolated sandbox first. Every CodexPro tool requires an explicit sandbox_id. Bash is unrestricted inside the sandbox but never has host shell or host Docker access.', }, ); const codexTools = dependencies.codexProTools.map(scopedCodexProTool); const codexToolNames = new Set(codexTools.map((tool) => tool.name)); - server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...managementTools, ...codexTools] })); + server.setRequestHandler(ListToolsRequestSchema, () => + Promise.resolve({ tools: [...managementTools, ...codexTools] }), + ); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const args = objectArgs(request.params.arguments); switch (request.params.name) { - case "sandbox_create": { - const modeValue = optionalString(args, "workspace_mode"); - const mode = modeValue as "managed" | "clone" | "direct" | undefined; - if (mode && mode !== "managed" && mode !== "clone" && mode !== "direct") throw new Error("workspace_mode must be managed, clone, or direct"); - return jsonResult(await dependencies.sandboxes.create(dependencies.principalId, { - workspaceId: optionalString(args, "workspace_id"), - workspacePath: optionalString(args, "workspace_path"), - workspaceMode: mode, - })); + case 'sandbox_create': { + const modeValue = optionalString(args, 'workspace_mode'); + const mode = modeValue as 'managed' | 'clone' | 'direct' | undefined; + if (mode && mode !== 'managed' && mode !== 'clone' && mode !== 'direct') { + throw new Error('workspace_mode must be managed, clone, or direct'); + } + return jsonResult( + await dependencies.sandboxes.create(dependencies.principalId, { + workspaceId: optionalString(args, 'workspace_id'), + workspacePath: optionalString(args, 'workspace_path'), + workspaceMode: mode, + }), + ); } - case "sandbox_list": + case 'sandbox_list': return jsonResult({ sandboxes: dependencies.sandboxes.list(dependencies.principalId) }); - case "sandbox_get": - return jsonResult(dependencies.sandboxes.get(dependencies.principalId, optionalString(args, "sandbox_id") ?? "")); - case "sandbox_expose": - return jsonResult(await dependencies.sandboxes.expose( - dependencies.principalId, - optionalString(args, "sandbox_id") ?? "", - requiredNumber(args, "port"), - )); - case "sandbox_destroy": - return jsonResult(await dependencies.sandboxes.destroy(dependencies.principalId, optionalString(args, "sandbox_id") ?? "")); - case "workspace_list": + case 'sandbox_get': + return jsonResult( + dependencies.sandboxes.get( + dependencies.principalId, + optionalString(args, 'sandbox_id') ?? '', + ), + ); + case 'sandbox_expose': + return jsonResult( + await dependencies.sandboxes.expose( + dependencies.principalId, + optionalString(args, 'sandbox_id') ?? '', + requiredNumber(args, 'port'), + ), + ); + case 'sandbox_destroy': + return jsonResult( + await dependencies.sandboxes.destroy( + dependencies.principalId, + optionalString(args, 'sandbox_id') ?? '', + ), + ); + case 'workspace_list': return jsonResult({ workspaces: dependencies.workspaces.list(dependencies.principalId) }); default: { - if (!codexToolNames.has(request.params.name)) throw new Error(`Unknown tool: ${request.params.name}`); - const sandboxId = optionalString(args, "sandbox_id"); - if (!sandboxId) throw new Error("sandbox_id is required"); - if ("workspace_id" in args) throw new Error("CodexPro workspace_id is internal; select the target with sandbox_id"); + if (!codexToolNames.has(request.params.name)) { + throw new Error(`Unknown tool: ${request.params.name}`); + } + const sandboxId = optionalString(args, 'sandbox_id'); + if (!sandboxId) { + throw new Error('sandbox_id is required'); + } + if ('workspace_id' in args) { + throw new Error('CodexPro workspace_id is internal; select the target with sandbox_id'); + } const { sandbox_id: _sandboxId, ...upstreamArgs } = args; - return await dependencies.codexPro.call(dependencies.principalId, sandboxId, request.params.name, upstreamArgs); + return await dependencies.codexPro.call( + dependencies.principalId, + sandboxId, + request.params.name, + upstreamArgs, + ); } } } catch (error) { diff --git a/src/mcp/gateway.ts b/src/mcp/gateway.ts index 6caa848..6965331 100644 --- a/src/mcp/gateway.ts +++ b/src/mcp/gateway.ts @@ -1,11 +1,11 @@ -import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import type { AppConfig } from "../config.js"; -import type { AuthProvider } from "../auth/provider.js"; -import { sessionlessDiscoverResponse } from "./compatibility.js"; -import { createControlServer, type ControlServerDependencies } from "./control-server.js"; +import http, { type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { AuthProvider } from '../auth/provider.js'; +import type { AppConfig } from '../config.js'; +import { sessionlessDiscoverResponse } from './compatibility.js'; +import { type ControlServerDependencies, createControlServer } from './control-server.js'; -type ControlServerWithoutPrincipal = Omit; +type ControlServerWithoutPrincipal = Omit; export interface GatewayDependencies { readonly authProvider: AuthProvider; @@ -15,20 +15,22 @@ export interface GatewayDependencies { function sendJson(response: ServerResponse, statusCode: number, value: unknown): void { const payload = JSON.stringify(value); response.writeHead(statusCode, { - "cache-control": "no-store", - "content-length": Buffer.byteLength(payload), - "content-type": "application/json", + 'cache-control': 'no-store', + 'content-length': Buffer.byteLength(payload), + 'content-type': 'application/json', }); response.end(payload); } async function readBody(request: IncomingMessage, maxBodyBytes: number): Promise { - const chunks: Buffer[] = []; + const chunks: Uint8Array[] = []; let receivedBytes = 0; for await (const chunk of request) { const buffer = Buffer.from(chunk); receivedBytes += buffer.length; - if (receivedBytes > maxBodyBytes) throw new Error("payload_too_large"); + if (receivedBytes > maxBodyBytes) { + throw new Error('payload_too_large'); + } chunks.push(buffer); } return Buffer.concat(chunks); @@ -40,17 +42,21 @@ async function handleRequest( config: AppConfig, dependencies: GatewayDependencies, ): Promise { - const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? config.host}`); - if (requestUrl.pathname === "/healthz") { - sendJson(response, 200, { status: "ok" }); + const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? config.host}`); + if (requestUrl.pathname === '/healthz') { + sendJson(response, 200, { status: 'ok' }); return; } - if (requestUrl.pathname !== "/mcp") { - sendJson(response, 404, { error: "not_found" }); + if (requestUrl.pathname !== '/mcp') { + sendJson(response, 404, { error: 'not_found' }); return; } - if (request.method !== "POST") { - sendJson(response, 405, { jsonrpc: "2.0", id: null, error: { code: -32000, message: "Method not allowed" } }); + if (request.method !== 'POST') { + sendJson(response, 405, { + jsonrpc: '2.0', + id: null, + error: { code: -32000, message: 'Method not allowed' }, + }); return; } @@ -63,14 +69,21 @@ async function handleRequest( } let parsedBody: unknown; try { - parsedBody = JSON.parse(body.toString("utf8")); + parsedBody = JSON.parse(body.toString('utf8')); } catch { - sendJson(response, 400, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); + sendJson(response, 400, { + jsonrpc: '2.0', + id: null, + error: { code: -32700, message: 'Parse error' }, + }); return; } const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - const mcpServer = createControlServer({ ...dependencies.controlServer, principalId: principal.id }); + const mcpServer = createControlServer({ + ...dependencies.controlServer, + principalId: principal.id, + }); await mcpServer.connect(transport); try { await transport.handleRequest(request, response, parsedBody); @@ -83,11 +96,18 @@ async function handleRequest( export function createGateway(config: AppConfig, dependencies: GatewayDependencies): Server { return http.createServer((request, response) => { handleRequest(request, response, config, dependencies).catch((error: unknown) => { - if (response.writableEnded || response.headersSent) return; - const status = error instanceof Error && error.message === "payload_too_large" ? 413 : 500; - const message = status === 413 ? "Payload too large" : "Internal chat2shell error"; - sendJson(response, status, { error: status === 413 ? "payload_too_large" : "internal_error", message }); - if (status === 500) console.error(error); + if (response.writableEnded || response.headersSent) { + return; + } + const status = error instanceof Error && error.message === 'payload_too_large' ? 413 : 500; + const message = status === 413 ? 'Payload too large' : 'Internal chat2shell error'; + sendJson(response, status, { + error: status === 413 ? 'payload_too_large' : 'internal_error', + message, + }); + if (status === 500) { + console.error(error); + } }); }); } diff --git a/src/mcp/main.ts b/src/mcp/main.ts index ee4b85f..0efc92c 100644 --- a/src/mcp/main.ts +++ b/src/mcp/main.ts @@ -1,12 +1,12 @@ -import { SingleUserAuthProvider } from "../auth/single-user-provider.js"; -import { CodexProClientPool } from "../codexpro/client-pool.js"; -import { codexProToolManifest } from "../codexpro/tool-manifest.js"; -import { loadAppConfig } from "../config.js"; -import { SbxDriver } from "../sandbox/sbx-driver.js"; -import { SandboxService } from "../sandbox/service.js"; -import { StateDatabase } from "../state/database.js"; -import { WorkspaceService } from "../workspaces/service.js"; -import { createGateway } from "./gateway.js"; +import { SingleUserAuthProvider } from '../auth/single-user-provider.js'; +import { CodexProClientPool } from '../codexpro/client-pool.js'; +import { codexProToolManifest } from '../codexpro/tool-manifest.js'; +import { loadAppConfig } from '../config.js'; +import { SbxDriver } from '../sandbox/sbx-driver.js'; +import { SandboxService } from '../sandbox/service.js'; +import { StateDatabase } from '../state/database.js'; +import { WorkspaceService } from '../workspaces/service.js'; +import { createGateway } from './gateway.js'; const config = loadAppConfig(); const database = new StateDatabase(config.databasePath); @@ -37,19 +37,34 @@ server.listen(config.port, config.host, () => { }); const reaper = setInterval(() => { - sandboxes.reap().catch((error) => console.error("[chat2shell] reaper failed", error)); + sandboxes.reap().catch((error) => console.error('[chat2shell] reaper failed', error)); }, config.reaperIntervalMs); reaper.unref(); let shuttingDown = false; + async function shutdown(): Promise { - if (shuttingDown) return; + if (shuttingDown) { + return; + } shuttingDown = true; clearInterval(reaper); - await new Promise((resolve) => server.close(() => resolve())); + await new Promise((resolve) => { + server.close(() => resolve()); + }); await codexPro.closeAll(); database.close(); } -process.on("SIGINT", () => { shutdown().finally(() => process.exit(0)); }); -process.on("SIGTERM", () => { shutdown().finally(() => process.exit(0)); }); +function requestShutdown(): void { + void shutdown().then( + () => process.exit(0), + (error: unknown) => { + console.error('[chat2shell] shutdown failed', error); + process.exit(1); + }, + ); +} + +process.on('SIGINT', requestShutdown); +process.on('SIGTERM', requestShutdown); diff --git a/src/sandbox/sbx-driver.ts b/src/sandbox/sbx-driver.ts index f6d8d76..fed7fb7 100644 --- a/src/sandbox/sbx-driver.ts +++ b/src/sandbox/sbx-driver.ts @@ -1,7 +1,7 @@ -import { execFile, spawn, type ChildProcess } from "node:child_process"; -import { createServer } from "node:net"; -import { promisify } from "node:util"; -import type { Workspace } from "../domain/types.js"; +import { type ChildProcess, execFile, spawn } from 'node:child_process'; +import { createServer } from 'node:net'; +import { promisify } from 'node:util'; +import type { Workspace } from '../domain/types.js'; const execFileAsync = promisify(execFile); @@ -29,7 +29,10 @@ export interface PublishedPort { export interface SandboxDriver { assertReady(): Promise; - create(runtimeName: string, workspace: Workspace): Promise<{ endpoint: string; runtimeRoot: string }>; + create( + runtimeName: string, + workspace: Workspace, + ): Promise<{ endpoint: string; runtimeRoot: string }>; startCodexPro(runtimeName: string, runtimeRoot: string, authToken: string): Promise; waitUntilHealthy(endpoint: string, authToken: string, timeoutMs?: number): Promise; isHealthy(endpoint: string, authToken: string): Promise; @@ -51,53 +54,100 @@ export class SbxDriver implements SandboxDriver { } async assertReady(): Promise { - const { stdout } = await this.#run(["template", "ls", "--json"]); + const { stdout } = await this.#run(['template', 'ls', '--json']); const images = JSON.parse(stdout) as { images: Array<{ repository: string; tag: string }> }; - const requested = this.#template.replace(/^docker\.io\/library\//, ""); - const present = images.images.some((image) => `${image.repository}:${image.tag}`.replace(/^docker\.io\/library\//, "") === requested); - if (!present) throw new Error(`Missing sandbox template ${this.#template}. Run scripts/setup-template.sh first.`); + const requested = this.#template.replace(/^docker\.io\/library\//, ''); + const present = images.images.some( + (image) => + `${image.repository}:${image.tag}`.replace(/^docker\.io\/library\//, '') === requested, + ); + if (!present) { + throw new Error( + `Missing sandbox template ${this.#template}. Run scripts/setup-template.sh first.`, + ); + } } - async create(runtimeName: string, workspace: Workspace): Promise<{ endpoint: string; runtimeRoot: string }> { - const args = ["create", "--quiet", "--name", runtimeName, "--template", this.#template, - "--publish", String(this.#sandboxPort), - "--deny-network", "openrouter.ai"]; - if (workspace.mode === "clone") args.push("--clone"); - args.push("shell", workspace.root); + async create( + runtimeName: string, + workspace: Workspace, + ): Promise<{ endpoint: string; runtimeRoot: string }> { + const args = [ + 'create', + '--quiet', + '--name', + runtimeName, + '--template', + this.#template, + '--publish', + String(this.#sandboxPort), + '--deny-network', + 'openrouter.ai', + ]; + if (workspace.mode === 'clone') { + args.push('--clone'); + } + args.push('shell', workspace.root); await this.#run(args, 180_000); const [{ stdout: rootOutput }, { stdout: portsOutput }] = await Promise.all([ - this.#run(["exec", runtimeName, "pwd"]), - this.#run(["ports", runtimeName, "--json"]), + this.#run(['exec', runtimeName, 'pwd']), + this.#run(['ports', runtimeName, '--json']), ]); const ports = JSON.parse(portsOutput) as SbxPort[]; - const port = ports.find((candidate) => candidate.host_ip === "127.0.0.1" && candidate.sandbox_port === this.#sandboxPort); - if (!port) throw new Error(`Sandbox ${runtimeName} did not publish port ${this.#sandboxPort} on IPv4 loopback`); + const port = ports.find( + (candidate) => + candidate.host_ip === '127.0.0.1' && candidate.sandbox_port === this.#sandboxPort, + ); + if (!port) { + throw new Error( + `Sandbox ${runtimeName} did not publish port ${this.#sandboxPort} on IPv4 loopback`, + ); + } return { endpoint: `http://127.0.0.1:${port.host_port}/mcp`, runtimeRoot: rootOutput.trim() }; } async startCodexPro(runtimeName: string, runtimeRoot: string, authToken: string): Promise { - if (this.#codexProProcesses.has(runtimeName)) throw new Error(`CodexPro is already running in ${runtimeName}`); - const child = spawn(this.#binary, [ - "exec", "-i", "-e", "CODEXPRO_HTTP_TOKEN", runtimeName, - "codexpro-mcp-http", - "--root", runtimeRoot, - "--allow-root", runtimeRoot, - "--host", "0.0.0.0", - "--port", String(this.#sandboxPort), - "--bash", "full", - "--write", "workspace", - "--tool-mode", "standard", - ], { - env: { ...process.env, CODEXPRO_HTTP_TOKEN: authToken }, - stdio: ["pipe", "ignore", "inherit"], - }); + if (this.#codexProProcesses.has(runtimeName)) { + throw new Error(`CodexPro is already running in ${runtimeName}`); + } + const child = spawn( + this.#binary, + [ + 'exec', + '-i', + '-e', + 'CODEXPRO_HTTP_TOKEN', + runtimeName, + 'codexpro-mcp-http', + '--root', + runtimeRoot, + '--allow-root', + runtimeRoot, + '--host', + '0.0.0.0', + '--port', + String(this.#sandboxPort), + '--bash', + 'full', + '--write', + 'workspace', + '--tool-mode', + 'standard', + ], + { + env: { ...process.env, CODEXPRO_HTTP_TOKEN: authToken }, + stdio: ['pipe', 'ignore', 'inherit'], + }, + ); this.#codexProProcesses.set(runtimeName, child); - child.once("exit", () => { - if (this.#codexProProcesses.get(runtimeName) === child) this.#codexProProcesses.delete(runtimeName); + child.once('exit', () => { + if (this.#codexProProcesses.get(runtimeName) === child) { + this.#codexProProcesses.delete(runtimeName); + } }); await new Promise((resolve, reject) => { - child.once("spawn", resolve); - child.once("error", reject); + child.once('spawn', resolve); + child.once('error', reject); }).catch((error) => { this.#codexProProcesses.delete(runtimeName); throw error; @@ -105,31 +155,37 @@ export class SbxDriver implements SandboxDriver { } async waitUntilHealthy(endpoint: string, authToken: string, timeoutMs = 15_000): Promise { - const healthUrl = new URL("/healthz", endpoint); + const healthUrl = new URL('/healthz', endpoint); const deadline = Date.now() + timeoutMs; - let lastError = "not ready"; + let lastError = 'not ready'; while (Date.now() < deadline) { try { const response = await fetch(healthUrl, { headers: { authorization: `Bearer ${authToken}` }, signal: AbortSignal.timeout(1_000), }); - if (response.ok) return; + if (response.ok) { + return; + } lastError = `HTTP ${response.status}`; } catch (error) { lastError = error instanceof Error ? error.message : String(error); } - await new Promise((resolve) => setTimeout(resolve, 150)); + await new Promise((resolve) => { + setTimeout(resolve, 150); + }); } throw new Error(`CodexPro did not become healthy: ${lastError}`); } async isHealthy(endpoint: string, authToken: string): Promise { try { - return (await fetch(new URL("/healthz", endpoint), { - headers: { authorization: `Bearer ${authToken}` }, - signal: AbortSignal.timeout(1_000), - })).ok; + return ( + await fetch(new URL('/healthz', endpoint), { + headers: { authorization: `Bearer ${authToken}` }, + signal: AbortSignal.timeout(1_000), + }) + ).ok; } catch { return false; } @@ -137,57 +193,83 @@ export class SbxDriver implements SandboxDriver { async expose(runtimeName: string, sandboxPort: number): Promise { const existing = await this.#publishedPort(runtimeName, sandboxPort); - if (existing) return existing; + if (existing) { + return existing; + } const hostPort = await this.#availableHostPort(); - await this.#run(["ports", runtimeName, "--publish", `0.0.0.0:${hostPort}:${sandboxPort}/tcp4`]); + await this.#run(['ports', runtimeName, '--publish', `0.0.0.0:${hostPort}:${sandboxPort}/tcp4`]); const published = await this.#publishedPort(runtimeName, sandboxPort); - if (!published) throw new Error(`Sandbox ${runtimeName} did not publish port ${sandboxPort}`); + if (!published) { + throw new Error(`Sandbox ${runtimeName} did not publish port ${sandboxPort}`); + } return published; } async remove(runtimeName: string): Promise { if ((await this.list()).some((runtime) => runtime.name === runtimeName)) { - await this.#run(["rm", "--force", runtimeName], 120_000); + await this.#run(['rm', '--force', runtimeName], 120_000); } const child = this.#codexProProcesses.get(runtimeName); this.#codexProProcesses.delete(runtimeName); - child?.kill("SIGTERM"); + child?.kill('SIGTERM'); } async list(): Promise { - const { stdout } = await this.#run(["ls", "--json"]); + const { stdout } = await this.#run(['ls', '--json']); const parsed = JSON.parse(stdout) as { sandboxes: SbxListItem[] }; return parsed.sandboxes.map(({ name, status }) => ({ name, status })); } - async #publishedPort(runtimeName: string, sandboxPort: number): Promise { - const { stdout } = await this.#run(["ports", runtimeName, "--json"]); + async #publishedPort( + runtimeName: string, + sandboxPort: number, + ): Promise { + const { stdout } = await this.#run(['ports', runtimeName, '--json']); const ports = JSON.parse(stdout) as SbxPort[]; - const port = ports.find((candidate) => candidate.host_ip === "0.0.0.0" && candidate.sandbox_port === sandboxPort && candidate.protocol === "tcp4"); + const port = ports.find( + (candidate) => + candidate.host_ip === '0.0.0.0' && + candidate.sandbox_port === sandboxPort && + candidate.protocol === 'tcp4', + ); return port ? { sandboxPort: port.sandbox_port, hostPort: port.host_port } : undefined; } async #availableHostPort(): Promise { const server = createServer(); return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "0.0.0.0", () => { + server.once('error', reject); + server.listen(0, '0.0.0.0', () => { const address = server.address(); server.close((error) => { - if (error) reject(error); - else if (!address || typeof address === "string") reject(new Error("Could not allocate a host port")); - else resolve(address.port); + if (error) { + reject(error); + } else if (!address || typeof address === 'string') { + reject(new Error('Could not allocate a host port')); + } else { + resolve(address.port); + } }); }); }); } - async #run(args: readonly string[], timeout = 30_000): Promise<{ stdout: string; stderr: string }> { + async #run( + args: readonly string[], + timeout = 30_000, + ): Promise<{ stdout: string; stderr: string }> { try { - return await execFileAsync(this.#binary, [...args], { encoding: "utf8", timeout, maxBuffer: 20 * 1024 * 1024 }); + return await execFileAsync(this.#binary, [...args], { + encoding: 'utf8', + timeout, + maxBuffer: 20 * 1024 * 1024, + }); } catch (error) { const detail = error as Error & { stderr?: string; stdout?: string }; - throw new Error(`sbx ${args[0]} failed: ${detail.stderr?.trim() || detail.stdout?.trim() || detail.message}`); + throw new Error( + `sbx ${args[0]} failed: ${detail.stderr?.trim() || detail.stdout?.trim() || detail.message}`, + { cause: error }, + ); } } } diff --git a/src/sandbox/service.ts b/src/sandbox/service.ts index f264b3b..01366fb 100644 --- a/src/sandbox/service.ts +++ b/src/sandbox/service.ts @@ -1,10 +1,18 @@ -import { randomBytes } from "node:crypto"; -import type { AppConfig } from "../config.js"; -import { createId } from "../domain/ids.js"; -import type { Approval, Sandbox, SandboxCreateResult, SandboxPortExposure, SandboxSummary, Workspace, WorkspaceMode } from "../domain/types.js"; -import type { StateDatabase } from "../state/database.js"; -import type { WorkspaceService } from "../workspaces/service.js"; -import type { SandboxDriver } from "./sbx-driver.js"; +import { randomBytes } from 'node:crypto'; +import type { AppConfig } from '../config.js'; +import type { + Approval, + Sandbox, + SandboxCreateResult, + SandboxPortExposure, + SandboxSummary, + Workspace, + WorkspaceMode, +} from '../domain/types.js'; +import type { StateDatabase } from '../state/database.js'; +import type { WorkspaceService } from '../workspaces/service.js'; +import { createId } from '../domain/ids.js'; +import type { SandboxDriver } from './sbx-driver.js'; export interface CreateSandboxRequest { readonly workspaceId?: string; @@ -13,7 +21,7 @@ export interface CreateSandboxRequest { } function isApproval(value: Workspace | Approval): value is Approval { - return "requestedPath" in value; + return 'requestedPath' in value; } export class SandboxService { @@ -44,38 +52,50 @@ export class SandboxService { } async create(ownerId: string, request: CreateSandboxRequest): Promise { - if (request.workspaceId && request.workspacePath) throw new Error("Specify workspace_id or workspace_path, not both"); + if (request.workspaceId && request.workspacePath) { + throw new Error('Specify workspace_id or workspace_path, not both'); + } let workspace: Workspace; if (request.workspaceId) { workspace = this.#workspaces.getApproved(ownerId, request.workspaceId); if (request.workspaceMode && request.workspaceMode !== workspace.mode) { - throw new Error(`workspace_mode=${request.workspaceMode} does not match approved workspace mode ${workspace.mode}`); + throw new Error( + `workspace_mode=${request.workspaceMode} does not match approved workspace mode ${workspace.mode}`, + ); } } else if (request.workspacePath) { - const mode = request.workspaceMode ?? "clone"; - if (mode === "managed") throw new Error("workspace_mode=managed cannot be used with workspace_path"); + const mode = request.workspaceMode ?? 'clone'; + if (mode === 'managed') { + throw new Error('workspace_mode=managed cannot be used with workspace_path'); + } const candidate = this.#workspaces.requestHost(ownerId, request.workspacePath, mode); - if (isApproval(candidate)) return { status: "approval_required", approval: candidate }; + if (isApproval(candidate)) { + return { status: 'approval_required', approval: candidate }; + } workspace = candidate; } else { - if (request.workspaceMode && request.workspaceMode !== "managed") { - throw new Error("workspace_mode requires workspace_path or workspace_id"); + if (request.workspaceMode && request.workspaceMode !== 'managed') { + throw new Error('workspace_mode requires workspace_path or workspace_id'); } workspace = this.#workspaces.createManaged(ownerId); } const active = this.#database.findActiveSandbox(ownerId, workspace.id); - if (active?.status === "running") return { status: "reused", sandbox: this.#summarize(active) }; - if (active) throw new Error(`Workspace already has a sandbox in ${active.status} state: ${active.id}`); + if (active?.status === 'running') { + return { status: 'reused', sandbox: this.#summarize(active) }; + } + if (active) { + throw new Error(`Workspace already has a sandbox in ${active.status} state: ${active.id}`); + } const now = this.#now(); - const id = createId("sbx"); + const id = createId('sbx'); const sandbox: Sandbox = { id, ownerId, workspaceId: workspace.id, runtimeName: `c2s-${id.slice(4, 25)}`, - status: "creating", + status: 'creating', createdAt: now, lastActivityAt: now, expiresAt: now + this.#config.idleTimeoutMs, @@ -84,17 +104,17 @@ export class SandboxService { try { const runtime = await this.#driver.create(sandbox.runtimeName, workspace); - const authToken = randomBytes(32).toString("hex"); + const authToken = randomBytes(32).toString('hex'); await this.#driver.startCodexPro(sandbox.runtimeName, runtime.runtimeRoot, authToken); await this.#driver.waitUntilHealthy(runtime.endpoint, authToken); - const running: Sandbox = { ...sandbox, ...runtime, authToken, status: "running" }; + const running: Sandbox = { ...sandbox, ...runtime, authToken, status: 'running' }; this.#database.saveSandbox(running); - return { status: "created", sandbox: this.#summarize(running) }; + return { status: 'created', sandbox: this.#summarize(running) }; } catch (error) { const message = error instanceof Error ? error.message : String(error); await this.#driver.remove(sandbox.runtimeName).catch(() => undefined); const destroyedAt = this.#now(); - this.#database.saveSandbox({ ...sandbox, status: "failed", error: message, destroyedAt }); + this.#database.saveSandbox({ ...sandbox, status: 'failed', error: message, destroyedAt }); this.#retainManagedWorkspace(sandbox.workspaceId, destroyedAt); throw error; } @@ -106,26 +126,40 @@ export class SandboxService { get(ownerId: string, sandboxId: string): SandboxSummary { const sandbox = this.#database.getSandbox(sandboxId, ownerId); - if (!sandbox) throw new Error(`Unknown sandbox: ${sandboxId}`); + if (!sandbox) { + throw new Error(`Unknown sandbox: ${sandboxId}`); + } return this.#summarize(sandbox); } async readyForTool(ownerId: string, sandboxId: string): Promise { - return this.withReady(ownerId, sandboxId, async (sandbox) => sandbox); + return this.withReady(ownerId, sandboxId, (sandbox) => Promise.resolve(sandbox)); } async expose(ownerId: string, sandboxId: string, port: number): Promise { - if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("port must be an integer from 1 to 65535"); + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + throw new Error('port must be an integer from 1 to 65535'); + } return this.withReady(ownerId, sandboxId, async (sandbox) => { const published = await this.#driver.expose(sandbox.runtimeName, port); return { sandboxId, ...published }; }); } - async withReady(ownerId: string, sandboxId: string, operation: (sandbox: Sandbox) => Promise): Promise { + async withReady( + ownerId: string, + sandboxId: string, + operation: (sandbox: Sandbox) => Promise, + ): Promise { return this.withLock(sandboxId, async () => { const sandbox = this.#database.getSandbox(sandboxId, ownerId); - if (!sandbox || sandbox.status !== "running" || !sandbox.endpoint || !sandbox.authToken || !sandbox.runtimeRoot) { + if ( + !sandbox || + sandbox.status !== 'running' || + !sandbox.endpoint || + !sandbox.authToken || + !sandbox.runtimeRoot + ) { throw new Error(`Sandbox is not running: ${sandboxId}`); } const now = this.#now(); @@ -133,8 +167,8 @@ export class SandboxService { throw new Error(`Sandbox expired after inactivity: ${sandboxId}`); } if (!(await this.#driver.isHealthy(sandbox.endpoint, sandbox.authToken))) { - const message = "CodexPro is unavailable; destroy this sandbox and create a new one"; - this.#database.saveSandbox({ ...sandbox, status: "failed", error: message }); + const message = 'CodexPro is unavailable; destroy this sandbox and create a new one'; + this.#database.saveSandbox({ ...sandbox, status: 'failed', error: message }); throw new Error(`${message}: ${sandboxId}`); } try { @@ -153,8 +187,12 @@ export class SandboxService { async destroy(ownerId: string, sandboxId: string): Promise { return this.withLock(sandboxId, async () => { const sandbox = this.#database.getSandbox(sandboxId, ownerId); - if (!sandbox) throw new Error(`Unknown sandbox: ${sandboxId}`); - if (sandbox.status === "destroyed") return this.#summarize(sandbox); + if (!sandbox) { + throw new Error(`Unknown sandbox: ${sandboxId}`); + } + if (sandbox.status === 'destroyed') { + return this.#summarize(sandbox); + } return this.#removeSandbox(sandbox); }); } @@ -164,7 +202,9 @@ export class SandboxService { for (const candidate of this.#database.listExpiredSandboxes(this.#now())) { await this.withLock(candidate.id, async () => { const sandbox = this.#database.getSandbox(candidate.id, candidate.ownerId); - if (!sandbox || sandbox.status !== "running" || sandbox.expiresAt > this.#now()) return; + if (!sandbox || sandbox.status !== 'running' || sandbox.expiresAt > this.#now()) { + return; + } await this.#removeSandbox(sandbox); destroyed.push(sandbox.id); }); @@ -177,18 +217,28 @@ export class SandboxService { const runtimes = new Map((await this.#driver.list()).map((runtime) => [runtime.name, runtime])); for (const sandbox of this.#database.listActiveSandboxes()) { const runtime = runtimes.get(sandbox.runtimeName); - if (sandbox.status === "destroying") { - if (runtime) await this.#driver.remove(sandbox.runtimeName); + if (sandbox.status === 'destroying') { + if (runtime) { + await this.#driver.remove(sandbox.runtimeName); + } const destroyedAt = this.#now(); - this.#database.saveSandbox({ ...sandbox, status: "destroyed", endpoint: undefined, authToken: undefined, destroyedAt }); + this.#database.saveSandbox({ + ...sandbox, + status: 'destroyed', + endpoint: undefined, + authToken: undefined, + destroyedAt, + }); this.#retainManagedWorkspace(sandbox.workspaceId, destroyedAt); } else { - if (runtime) await this.#driver.remove(sandbox.runtimeName); + if (runtime) { + await this.#driver.remove(sandbox.runtimeName); + } const destroyedAt = this.#now(); this.#database.saveSandbox({ ...sandbox, - status: "failed", - error: "chat2shell restarted; destroy this sandbox and create a new one", + status: 'failed', + error: 'chat2shell restarted; destroy this sandbox and create a new one', destroyedAt, endpoint: undefined, authToken: undefined, @@ -201,7 +251,9 @@ export class SandboxService { async withLock(sandboxId: string, operation: () => Promise): Promise { const previous = this.#locks.get(sandboxId) ?? Promise.resolve(); let release!: () => void; - const current = new Promise((resolve) => { release = resolve; }); + const current = new Promise((resolve) => { + release = resolve; + }); const queued = previous.then(() => current); this.#locks.set(sandboxId, queued); await previous; @@ -209,19 +261,21 @@ export class SandboxService { return await operation(); } finally { release(); - if (this.#locks.get(sandboxId) === queued) this.#locks.delete(sandboxId); + if (this.#locks.get(sandboxId) === queued) { + this.#locks.delete(sandboxId); + } } } #retainManagedWorkspace(workspaceId: string, removedAt: number): void { const workspace = this.#database.getWorkspace(workspaceId); - if (workspace?.kind === "managed") { + if (workspace?.kind === 'managed') { this.#workspaces.retainManaged(workspace, removedAt + this.#config.workspaceRetentionMs); } } async #removeSandbox(sandbox: Sandbox): Promise { - this.#database.saveSandbox({ ...sandbox, status: "destroying" }); + this.#database.saveSandbox({ ...sandbox, status: 'destroying' }); try { await this.#driver.remove(sandbox.runtimeName); } catch (error) { @@ -229,9 +283,17 @@ export class SandboxService { this.#database.saveSandbox({ ...sandbox, error: `destroy failed: ${message}` }); throw error; } - for (const listener of this.#destroyListeners) await listener(sandbox.id); + for (const listener of this.#destroyListeners) { + await listener(sandbox.id); + } const destroyedAt = this.#now(); - const destroyed: Sandbox = { ...sandbox, status: "destroyed", destroyedAt, endpoint: undefined, authToken: undefined }; + const destroyed: Sandbox = { + ...sandbox, + status: 'destroyed', + destroyedAt, + endpoint: undefined, + authToken: undefined, + }; this.#database.saveSandbox(destroyed); this.#retainManagedWorkspace(sandbox.workspaceId, destroyedAt); return this.#summarize(destroyed); @@ -239,7 +301,9 @@ export class SandboxService { #summarize(sandbox: Sandbox): SandboxSummary { const workspace = this.#database.getWorkspace(sandbox.workspaceId); - if (!workspace) throw new Error(`Sandbox ${sandbox.id} references a missing workspace`); + if (!workspace) { + throw new Error(`Sandbox ${sandbox.id} references a missing workspace`); + } return { id: sandbox.id, status: sandbox.status, diff --git a/src/state/database.ts b/src/state/database.ts index 111fcc5..4e6f6f5 100644 --- a/src/state/database.ts +++ b/src/state/database.ts @@ -1,26 +1,26 @@ -import fs from "node:fs"; -import path from "node:path"; -import { DatabaseSync } from "node:sqlite"; -import type { Approval, Sandbox, SandboxStatus, Workspace } from "../domain/types.js"; +import fs from 'node:fs'; +import path from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; +import type { Approval, Sandbox, SandboxStatus, Workspace } from '../domain/types.js'; type SqlValue = string | number | null; function optionalNumber(value: unknown): number | undefined { - return typeof value === "number" ? value : undefined; + return typeof value === 'number' ? value : undefined; } function optionalString(value: unknown): string | undefined { - return typeof value === "string" ? value : undefined; + return typeof value === 'string' ? value : undefined; } function workspaceFromRow(row: Record): Workspace { return { id: String(row.id), ownerId: String(row.owner_id), - kind: row.kind as Workspace["kind"], - mode: row.mode as Workspace["mode"], + kind: row.kind as Workspace['kind'], + mode: row.mode as Workspace['mode'], root: String(row.root), - status: row.status as Workspace["status"], + status: row.status as Workspace['status'], createdAt: Number(row.created_at), retainedUntil: optionalNumber(row.retained_until), }; @@ -31,8 +31,8 @@ function approvalFromRow(row: Record): Approval { id: String(row.id), ownerId: String(row.owner_id), requestedPath: String(row.requested_path), - mode: row.mode as Approval["mode"], - status: row.status as Approval["status"], + mode: row.mode as Approval['mode'], + status: row.status as Approval['status'], workspaceId: optionalString(row.workspace_id), createdAt: Number(row.created_at), decidedAt: optionalNumber(row.decided_at), @@ -63,8 +63,12 @@ export class StateDatabase { constructor(databasePath: string) { fs.mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 }); this.#database = new DatabaseSync(databasePath); - if (databasePath !== ":memory:") fs.chmodSync(databasePath, 0o600); - this.#database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;"); + if (databasePath !== ':memory:') { + fs.chmodSync(databasePath, 0o600); + } + this.#database.exec( + 'PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;', + ); this.#migrate(); } @@ -118,121 +122,197 @@ export class StateDatabase { } insertWorkspace(workspace: Workspace): void { - this.#database.prepare(`INSERT INTO workspaces + this.#database + .prepare(`INSERT INTO workspaces (id, owner_id, kind, mode, root, status, created_at, retained_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) - .run(workspace.id, workspace.ownerId, workspace.kind, workspace.mode, workspace.root, workspace.status, workspace.createdAt, workspace.retainedUntil ?? null); + .run( + workspace.id, + workspace.ownerId, + workspace.kind, + workspace.mode, + workspace.root, + workspace.status, + workspace.createdAt, + workspace.retainedUntil ?? null, + ); } - updateWorkspaceStatus(id: string, status: Workspace["status"], retainedUntil?: number): void { - this.#database.prepare("UPDATE workspaces SET status = ?, retained_until = ? WHERE id = ?") + updateWorkspaceStatus(id: string, status: Workspace['status'], retainedUntil?: number): void { + this.#database + .prepare('UPDATE workspaces SET status = ?, retained_until = ? WHERE id = ?') .run(status, retainedUntil ?? null, id); } - updateWorkspaceLocation(id: string, root: string, status: Workspace["status"], retainedUntil?: number): void { - this.#database.prepare("UPDATE workspaces SET root = ?, status = ?, retained_until = ? WHERE id = ?") + updateWorkspaceLocation( + id: string, + root: string, + status: Workspace['status'], + retainedUntil?: number, + ): void { + this.#database + .prepare('UPDATE workspaces SET root = ?, status = ?, retained_until = ? WHERE id = ?') .run(root, status, retainedUntil ?? null, id); } getWorkspace(id: string, ownerId?: string): Workspace | undefined { const row = ownerId - ? this.#database.prepare("SELECT * FROM workspaces WHERE id = ? AND owner_id = ?").get(id, ownerId) - : this.#database.prepare("SELECT * FROM workspaces WHERE id = ?").get(id); + ? this.#database + .prepare('SELECT * FROM workspaces WHERE id = ? AND owner_id = ?') + .get(id, ownerId) + : this.#database.prepare('SELECT * FROM workspaces WHERE id = ?').get(id); return row ? workspaceFromRow(row) : undefined; } - findWorkspace(ownerId: string, root: string, mode: Workspace["mode"]): Workspace | undefined { - const row = this.#database.prepare("SELECT * FROM workspaces WHERE owner_id = ? AND root = ? AND mode = ?") + findWorkspace(ownerId: string, root: string, mode: Workspace['mode']): Workspace | undefined { + const row = this.#database + .prepare('SELECT * FROM workspaces WHERE owner_id = ? AND root = ? AND mode = ?') .get(ownerId, root, mode); return row ? workspaceFromRow(row) : undefined; } listWorkspaces(ownerId: string): readonly Workspace[] { - return this.#database.prepare("SELECT * FROM workspaces WHERE owner_id = ? ORDER BY created_at DESC") - .all(ownerId).map(workspaceFromRow); + return this.#database + .prepare('SELECT * FROM workspaces WHERE owner_id = ? ORDER BY created_at DESC') + .all(ownerId) + .map(workspaceFromRow); } listExpiredRetainedWorkspaces(now: number): readonly Workspace[] { - return this.#database.prepare("SELECT * FROM workspaces WHERE status = 'retained' AND retained_until <= ?") - .all(now).map(workspaceFromRow); + return this.#database + .prepare("SELECT * FROM workspaces WHERE status = 'retained' AND retained_until <= ?") + .all(now) + .map(workspaceFromRow); } insertApproval(approval: Approval): void { - this.#database.prepare(`INSERT INTO approvals + this.#database + .prepare(`INSERT INTO approvals (id, owner_id, requested_path, mode, status, workspace_id, created_at, decided_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) - .run(approval.id, approval.ownerId, approval.requestedPath, approval.mode, approval.status, approval.workspaceId ?? null, approval.createdAt, approval.decidedAt ?? null); + .run( + approval.id, + approval.ownerId, + approval.requestedPath, + approval.mode, + approval.status, + approval.workspaceId ?? null, + approval.createdAt, + approval.decidedAt ?? null, + ); } getApproval(id: string): Approval | undefined { - const row = this.#database.prepare("SELECT * FROM approvals WHERE id = ?").get(id); + const row = this.#database.prepare('SELECT * FROM approvals WHERE id = ?').get(id); return row ? approvalFromRow(row) : undefined; } - findPendingApproval(ownerId: string, requestedPath: string, mode: Approval["mode"]): Approval | undefined { - const row = this.#database.prepare("SELECT * FROM approvals WHERE owner_id = ? AND requested_path = ? AND mode = ? AND status = 'pending'") + findPendingApproval( + ownerId: string, + requestedPath: string, + mode: Approval['mode'], + ): Approval | undefined { + const row = this.#database + .prepare( + "SELECT * FROM approvals WHERE owner_id = ? AND requested_path = ? AND mode = ? AND status = 'pending'", + ) .get(ownerId, requestedPath, mode); return row ? approvalFromRow(row) : undefined; } - decideApproval(id: string, status: "approved" | "rejected", workspaceId: string | undefined, decidedAt: number): void { - this.#database.prepare("UPDATE approvals SET status = ?, workspace_id = ?, decided_at = ? WHERE id = ?") + decideApproval( + id: string, + status: 'approved' | 'rejected', + workspaceId: string | undefined, + decidedAt: number, + ): void { + this.#database + .prepare('UPDATE approvals SET status = ?, workspace_id = ?, decided_at = ? WHERE id = ?') .run(status, workspaceId ?? null, decidedAt, id); } - listApprovals(status?: Approval["status"]): readonly Approval[] { + listApprovals(status?: Approval['status']): readonly Approval[] { const rows = status - ? this.#database.prepare("SELECT * FROM approvals WHERE status = ? ORDER BY created_at DESC").all(status) - : this.#database.prepare("SELECT * FROM approvals ORDER BY created_at DESC").all(); + ? this.#database + .prepare('SELECT * FROM approvals WHERE status = ? ORDER BY created_at DESC') + .all(status) + : this.#database.prepare('SELECT * FROM approvals ORDER BY created_at DESC').all(); return rows.map(approvalFromRow); } insertSandbox(sandbox: Sandbox): void { - this.#database.prepare(`INSERT INTO sandboxes + this.#database + .prepare(`INSERT INTO sandboxes (id, owner_id, workspace_id, runtime_name, runtime_root, status, endpoint, auth_token, error, created_at, last_activity_at, expires_at, destroyed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) .run(...this.#sandboxValues(sandbox)); } saveSandbox(sandbox: Sandbox): void { - this.#database.prepare(`UPDATE sandboxes SET + this.#database + .prepare(`UPDATE sandboxes SET owner_id = ?, workspace_id = ?, runtime_name = ?, runtime_root = ?, status = ?, endpoint = ?, auth_token = ?, error = ?, created_at = ?, last_activity_at = ?, expires_at = ?, destroyed_at = ? WHERE id = ?`) .run(...this.#sandboxValues(sandbox).slice(1), sandbox.id); } #sandboxValues(sandbox: Sandbox): SqlValue[] { - return [sandbox.id, sandbox.ownerId, sandbox.workspaceId, sandbox.runtimeName, sandbox.runtimeRoot ?? null, sandbox.status, - sandbox.endpoint ?? null, sandbox.authToken ?? null, sandbox.error ?? null, sandbox.createdAt, sandbox.lastActivityAt, - sandbox.expiresAt, sandbox.destroyedAt ?? null]; + return [ + sandbox.id, + sandbox.ownerId, + sandbox.workspaceId, + sandbox.runtimeName, + sandbox.runtimeRoot ?? null, + sandbox.status, + sandbox.endpoint ?? null, + sandbox.authToken ?? null, + sandbox.error ?? null, + sandbox.createdAt, + sandbox.lastActivityAt, + sandbox.expiresAt, + sandbox.destroyedAt ?? null, + ]; } getSandbox(id: string, ownerId?: string): Sandbox | undefined { const row = ownerId - ? this.#database.prepare("SELECT * FROM sandboxes WHERE id = ? AND owner_id = ?").get(id, ownerId) - : this.#database.prepare("SELECT * FROM sandboxes WHERE id = ?").get(id); + ? this.#database + .prepare('SELECT * FROM sandboxes WHERE id = ? AND owner_id = ?') + .get(id, ownerId) + : this.#database.prepare('SELECT * FROM sandboxes WHERE id = ?').get(id); return row ? sandboxFromRow(row) : undefined; } findActiveSandbox(ownerId: string, workspaceId: string): Sandbox | undefined { - const row = this.#database.prepare(`SELECT * FROM sandboxes + const row = this.#database + .prepare(`SELECT * FROM sandboxes WHERE owner_id = ? AND workspace_id = ? AND status IN ('creating', 'running', 'destroying') ORDER BY created_at DESC LIMIT 1`) .get(ownerId, workspaceId); return row ? sandboxFromRow(row) : undefined; } listCurrentSandboxes(ownerId: string): readonly Sandbox[] { - return this.#database.prepare("SELECT * FROM sandboxes WHERE owner_id = ? AND status != 'destroyed' ORDER BY created_at DESC") - .all(ownerId).map(sandboxFromRow); + return this.#database + .prepare( + "SELECT * FROM sandboxes WHERE owner_id = ? AND status != 'destroyed' ORDER BY created_at DESC", + ) + .all(ownerId) + .map(sandboxFromRow); } listActiveSandboxes(): readonly Sandbox[] { - return this.#database.prepare("SELECT * FROM sandboxes WHERE status IN ('creating', 'running', 'destroying') ORDER BY created_at DESC") - .all().map(sandboxFromRow); + return this.#database + .prepare( + "SELECT * FROM sandboxes WHERE status IN ('creating', 'running', 'destroying') ORDER BY created_at DESC", + ) + .all() + .map(sandboxFromRow); } listExpiredSandboxes(now: number): readonly Sandbox[] { - return this.#database.prepare("SELECT * FROM sandboxes WHERE status = 'running' AND expires_at <= ?") - .all(now).map(sandboxFromRow); + return this.#database + .prepare("SELECT * FROM sandboxes WHERE status = 'running' AND expires_at <= ?") + .all(now) + .map(sandboxFromRow); } } diff --git a/src/workspaces/policy.ts b/src/workspaces/policy.ts index 2dfcd92..01bd149 100644 --- a/src/workspaces/policy.ts +++ b/src/workspaces/policy.ts @@ -1,14 +1,22 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; const blockedNames = new Set([ - ".aws", ".azure", ".config", ".docker", ".gnupg", ".kube", ".local", ".secrets", ".ssh", + '.aws', + '.azure', + '.config', + '.docker', + '.gnupg', + '.kube', + '.local', + '.secrets', + '.ssh', ]); function isInside(candidate: string, root: string): boolean { const relative = path.relative(root, candidate); - return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative); + return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative); } export class HostPathPolicy { @@ -19,18 +27,27 @@ export class HostPathPolicy { } resolveAndValidate(requestedPath: string): string { - const expanded = requestedPath === "~" - ? os.homedir() - : requestedPath.startsWith("~/") ? path.join(os.homedir(), requestedPath.slice(2)) : requestedPath; + const expanded = + requestedPath === '~' + ? os.homedir() + : requestedPath.startsWith('~/') + ? path.join(os.homedir(), requestedPath.slice(2)) + : requestedPath; const resolved = fs.realpathSync.native(path.resolve(expanded)); const stat = fs.statSync(resolved); - if (!stat.isDirectory()) throw new Error("Workspace path must be a directory"); + if (!stat.isDirectory()) { + throw new Error('Workspace path must be a directory'); + } if (!this.#allowedRoots.some((root) => isInside(resolved, root))) { - throw new Error(`Workspace must be below an allowed host root: ${this.#allowedRoots.join(", ")}`); + throw new Error( + `Workspace must be below an allowed host root: ${this.#allowedRoots.join(', ')}`, + ); } const names = resolved.split(path.sep); const blocked = names.find((name) => blockedNames.has(name)); - if (blocked) throw new Error(`Workspace path contains a protected directory: ${blocked}`); + if (blocked) { + throw new Error(`Workspace path contains a protected directory: ${blocked}`); + } return resolved; } } diff --git a/src/workspaces/service.ts b/src/workspaces/service.ts index 3dff011..d2273fb 100644 --- a/src/workspaces/service.ts +++ b/src/workspaces/service.ts @@ -1,9 +1,9 @@ -import fs from "node:fs"; -import path from "node:path"; -import { createId } from "../domain/ids.js"; -import type { Approval, Workspace, WorkspaceMode } from "../domain/types.js"; -import type { StateDatabase } from "../state/database.js"; -import { HostPathPolicy } from "./policy.js"; +import fs from 'node:fs'; +import path from 'node:path'; +import type { Approval, Workspace, WorkspaceMode } from '../domain/types.js'; +import type { StateDatabase } from '../state/database.js'; +import { createId } from '../domain/ids.js'; +import { HostPathPolicy } from './policy.js'; export class WorkspaceService { readonly #database: StateDatabase; @@ -21,7 +21,7 @@ export class WorkspaceService { }) { this.#database = options.database; this.#workspaceRoot = options.workspaceRoot; - this.#trashRoot = path.join(options.dataRoot, "trash"); + this.#trashRoot = path.join(options.dataRoot, 'trash'); this.#policy = new HostPathPolicy(options.allowedHostRoots); this.#now = options.now ?? Date.now; fs.mkdirSync(this.#workspaceRoot, { recursive: true, mode: 0o700 }); @@ -29,11 +29,17 @@ export class WorkspaceService { } createManaged(ownerId: string): Workspace { - const id = createId("ws"); + const id = createId('ws'); const root = path.join(this.#workspaceRoot, id); fs.mkdirSync(root, { recursive: false, mode: 0o700 }); const workspace: Workspace = { - id, ownerId, kind: "managed", mode: "managed", root, status: "approved", createdAt: this.#now(), + id, + ownerId, + kind: 'managed', + mode: 'managed', + root, + status: 'approved', + createdAt: this.#now(), }; try { this.#database.insertWorkspace(workspace); @@ -44,25 +50,50 @@ export class WorkspaceService { } } - requestHost(ownerId: string, requestedPath: string, mode: Exclude): Workspace | Approval { + requestHost( + ownerId: string, + requestedPath: string, + mode: Exclude, + ): Workspace | Approval { const root = this.#policy.resolveAndValidate(requestedPath); const existing = this.#database.findWorkspace(ownerId, root, mode); - if (existing?.status === "approved") return existing; + if (existing?.status === 'approved') { + return existing; + } const pending = this.#database.findPendingApproval(ownerId, root, mode); - if (pending) return pending; + if (pending) { + return pending; + } const approval: Approval = { - id: createId("approval"), ownerId, requestedPath: root, mode, status: "pending", createdAt: this.#now(), + id: createId('approval'), + ownerId, + requestedPath: root, + mode, + status: 'pending', + createdAt: this.#now(), }; this.#database.insertApproval(approval); return approval; } - registerHost(ownerId: string, requestedPath: string, mode: Exclude): Workspace { + registerHost( + ownerId: string, + requestedPath: string, + mode: Exclude, + ): Workspace { const root = this.#policy.resolveAndValidate(requestedPath); const existing = this.#database.findWorkspace(ownerId, root, mode); - if (existing) return existing; + if (existing) { + return existing; + } const workspace: Workspace = { - id: createId("ws"), ownerId, kind: "host", mode, root, status: "approved", createdAt: this.#now(), + id: createId('ws'), + ownerId, + kind: 'host', + mode, + root, + status: 'approved', + createdAt: this.#now(), }; this.#database.insertWorkspace(workspace); return workspace; @@ -70,28 +101,38 @@ export class WorkspaceService { approve(approvalId: string): Workspace { const approval = this.#database.getApproval(approvalId); - if (!approval) throw new Error(`Unknown approval: ${approvalId}`); - if (approval.status !== "pending") throw new Error(`Approval is already ${approval.status}`); + if (!approval) { + throw new Error(`Unknown approval: ${approvalId}`); + } + if (approval.status !== 'pending') { + throw new Error(`Approval is already ${approval.status}`); + } const workspace = this.registerHost(approval.ownerId, approval.requestedPath, approval.mode); - this.#database.decideApproval(approval.id, "approved", workspace.id, this.#now()); + this.#database.decideApproval(approval.id, 'approved', workspace.id, this.#now()); return workspace; } reject(approvalId: string): Approval { const approval = this.#database.getApproval(approvalId); - if (!approval) throw new Error(`Unknown approval: ${approvalId}`); - if (approval.status !== "pending") throw new Error(`Approval is already ${approval.status}`); + if (!approval) { + throw new Error(`Unknown approval: ${approvalId}`); + } + if (approval.status !== 'pending') { + throw new Error(`Approval is already ${approval.status}`); + } const decidedAt = this.#now(); - this.#database.decideApproval(approval.id, "rejected", undefined, decidedAt); - return { ...approval, status: "rejected", decidedAt }; + this.#database.decideApproval(approval.id, 'rejected', undefined, decidedAt); + return { ...approval, status: 'rejected', decidedAt }; } getApproved(ownerId: string, workspaceId: string): Workspace { const workspace = this.#database.getWorkspace(workspaceId, ownerId); - if (!workspace || workspace.status === "trashed") throw new Error(`Unknown or unavailable workspace: ${workspaceId}`); - if (workspace.status === "retained") { - this.#database.updateWorkspaceStatus(workspace.id, "approved"); - return { ...workspace, status: "approved", retainedUntil: undefined }; + if (!workspace || workspace.status === 'trashed') { + throw new Error(`Unknown or unavailable workspace: ${workspaceId}`); + } + if (workspace.status === 'retained') { + this.#database.updateWorkspaceStatus(workspace.id, 'approved'); + return { ...workspace, status: 'approved', retainedUntil: undefined }; } return workspace; } @@ -101,15 +142,19 @@ export class WorkspaceService { } retainManaged(workspace: Workspace, retainedUntil: number): Workspace { - if (workspace.kind !== "managed") return workspace; - this.#database.updateWorkspaceStatus(workspace.id, "retained", retainedUntil); - return { ...workspace, status: "retained", retainedUntil }; + if (workspace.kind !== 'managed') { + return workspace; + } + this.#database.updateWorkspaceStatus(workspace.id, 'retained', retainedUntil); + return { ...workspace, status: 'retained', retainedUntil }; } trashExpired(now = this.#now()): readonly Workspace[] { const trashed: Workspace[] = []; for (const workspace of this.#database.listExpiredRetainedWorkspaces(now)) { - if (workspace.kind !== "managed") continue; + if (workspace.kind !== 'managed') { + continue; + } const source = fs.realpathSync.native(workspace.root); const workspaceRoot = fs.realpathSync.native(this.#workspaceRoot); if (path.dirname(source) !== workspaceRoot || path.basename(source) !== workspace.id) { @@ -117,8 +162,13 @@ export class WorkspaceService { } const destination = path.join(this.#trashRoot, `${workspace.id}-${now}`); fs.renameSync(source, destination); - this.#database.updateWorkspaceLocation(workspace.id, destination, "trashed"); - trashed.push({ ...workspace, root: destination, status: "trashed", retainedUntil: undefined }); + this.#database.updateWorkspaceLocation(workspace.id, destination, 'trashed'); + trashed.push({ + ...workspace, + root: destination, + status: 'trashed', + retainedUntil: undefined, + }); } return trashed; } diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..b5aaf78 --- /dev/null +++ b/test/README.md @@ -0,0 +1,51 @@ +# Tests + +The test suite is split by the boundary each test crosses. Keep tests in the narrowest category that still exercises the behavior realistically. + +## Unit + +`test/unit` covers small code units without external resources or cross-component runtime behavior. + +```bash +pnpm test:unit +``` + +Examples include configuration parsing, MCP compatibility helpers, tool schema transformation, and result normalization. + +## Integration + +`test/integration` connects multiple chat2shell components and may use local process resources such as temporary files, SQLite, or loopback HTTP. It must not require Docker Sandboxes (`sbx`) or a real CodexPro microVM. + +```bash +pnpm test:integration +``` + +The default test command runs unit and integration tests only: + +```bash +pnpm test +``` + +This is the test boundary used by normal GitHub CI. + +## E2E + +`test/e2e` exercises the public MCP boundary against real Docker Sandbox microVMs, CodexPro, the sandbox-private Docker Engine, and host port exposure. + +```bash +pnpm test:e2e +``` + +E2E tests require a trusted chat2shell host with the `sbx` executable and the `chat2shell-codexpro:0.30.0` template installed. The command fails when those prerequisites are unavailable; it does not silently skip the suite. + +Real E2E tests do not run on ordinary GitHub-hosted CI. Run them on a trusted development host before changes that affect the sandbox boundary, lifecycle, workspace modes, CodexPro routing, or port exposure. + +## Coverage + +Coverage uses Vitest's V8 provider for unit and integration tests: + +```bash +pnpm test:coverage +``` + +Coverage is diagnostic rather than a percentage gate. Security and lifecycle invariants are more important than maximizing line coverage. diff --git a/test/client-pool.test.ts b/test/client-pool.test.ts deleted file mode 100644 index c99b3ed..0000000 --- a/test/client-pool.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { normalizeWorkspaceIdentity } from "../src/codexpro/client-pool.js"; - -test("replaces CodexPro's internal workspace identity with the public chat2shell identity", () => { - const result = normalizeWorkspaceIdentity({ - content: [{ type: "text", text: "Workspace ws_internal selected; ws_internal is ready." }], - structuredContent: { - workspace_id: "ws_internal", - selected_workspace_id: "ws_internal", - root: "/workspace", - }, - }, "ws_public"); - - assert.deepEqual(result.structuredContent, { - workspace_id: "ws_public", - selected_workspace_id: "ws_public", - root: "/workspace", - }); - assert.equal(result.content[0]?.type, "text"); - assert.equal(result.content[0]?.type === "text" ? result.content[0].text : undefined, "Workspace ws_public selected; ws_public is ready."); -}); - -test("leaves results without a CodexPro workspace identity untouched", () => { - const result = { - content: [{ type: "text" as const, text: "No workspace identity" }], - structuredContent: { status: "ok" }, - }; - - assert.equal(normalizeWorkspaceIdentity(result, "ws_public"), result); -}); diff --git a/test/compatibility.test.ts b/test/compatibility.test.ts deleted file mode 100644 index 42f1922..0000000 --- a/test/compatibility.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { sessionlessDiscoverResponse } from "../src/mcp/compatibility.js"; - -test("returns method-not-found for a sessionless server/discover probe", () => { - const response = sessionlessDiscoverResponse( - {}, - Buffer.from('{"jsonrpc":"2.0","id":"probe","method":"server/discover","params":{}}'), - ); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: "probe", - error: { code: -32601, message: "Method not found" }, - }); -}); - -test("does not intercept established MCP sessions", () => { - const response = sessionlessDiscoverResponse( - { "mcp-session-id": "session-1" }, - Buffer.from('{"jsonrpc":"2.0","id":1,"method":"server/discover"}'), - ); - - assert.equal(response, undefined); -}); diff --git a/test/config.test.ts b/test/config.test.ts deleted file mode 100644 index 106a66c..0000000 --- a/test/config.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { loadAppConfig } from "../src/config.js"; - -test("uses the complete public lifecycle policy", () => { - const config = loadAppConfig({ HOME: "/tmp/chat2shell-config-test" }); - - assert.equal(config.idleTimeoutMs, 24 * 60 * 60_000); - assert.equal(config.workspaceRetentionMs, 30 * 24 * 60 * 60_000); - assert.equal("maxLifetimeMs" in config, false); - assert.equal("sandboxCpus" in config, false); - assert.equal("sandboxMemory" in config, false); -}); diff --git a/test/e2e/sandbox-lifecycle.test.ts b/test/e2e/sandbox-lifecycle.test.ts new file mode 100644 index 0000000..e4515b8 --- /dev/null +++ b/test/e2e/sandbox-lifecycle.test.ts @@ -0,0 +1,266 @@ +import type http from 'node:http'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import { once } from 'node:events'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { expect, test } from 'vitest'; +import type { AppConfig } from '../../src/config.js'; +import { SingleUserAuthProvider } from '../../src/auth/single-user-provider.js'; +import { CodexProClientPool } from '../../src/codexpro/client-pool.js'; +import { codexProToolManifest } from '../../src/codexpro/tool-manifest.js'; +import { createGateway } from '../../src/mcp/gateway.js'; +import { SbxDriver } from '../../src/sandbox/sbx-driver.js'; +import { SandboxService } from '../../src/sandbox/service.js'; +import { StateDatabase } from '../../src/state/database.js'; +import { WorkspaceService } from '../../src/workspaces/service.js'; + +function requireSbx(): void { + const result = spawnSync('sbx', ['--help'], { encoding: 'utf8' }); + if (result.error) { + throw new Error(`E2E requires the Docker Sandboxes sbx executable: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error(`E2E could not execute sbx: ${result.stderr.trim() || result.stdout.trim()}`); + } +} + +async function listen(server: http.Server): Promise { + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Expected a TCP server address'); + } + return address.port; +} + +async function rpc( + url: string, + id: number, + method: string, + params: Record = {}, +): Promise> { + const response = await fetch(url, { + body: JSON.stringify({ id, jsonrpc: '2.0', method, params }), + headers: { + 'accept': 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + method: 'POST', + }); + const text = await response.text(); + expect(response.status, text).toBe(200); + const data = text.split(/\r?\n/).find((line) => line.startsWith('data:')); + return JSON.parse(data ? data.slice(5).trim() : text) as Record; +} + +async function callTool( + url: string, + id: number, + name: string, + args: Record, +): Promise> { + const response = await rpc(url, id, 'tools/call', { arguments: args, name }); + return response.result as Record; +} + +test('routes full shell and private Docker only into a real microVM', async () => { + requireSbx(); + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-e2e-')); + const allowedRoot = path.join(base, 'host'); + fs.mkdirSync(allowedRoot); + const appConfig: AppConfig = { + allowedHostRoots: [allowedRoot], + dataRoot: path.join(base, 'data'), + databasePath: path.join(base, 'data', 'state', 'test.sqlite'), + host: '127.0.0.1', + idleTimeoutMs: 24 * 60 * 60_000, + maxBodyBytes: 20 * 1024 * 1024, + port: 0, + reaperIntervalMs: 60_000, + sandboxPort: 18_787, + sandboxTemplate: 'chat2shell-codexpro:0.30.0', + sbxBinary: 'sbx', + stateDir: path.join(base, 'data', 'state'), + workspaceRetentionMs: 30 * 24 * 60 * 60_000, + workspaceRoot: path.join(base, 'data', 'workspaces'), + }; + const database = new StateDatabase(appConfig.databasePath); + const workspaces = new WorkspaceService({ + allowedHostRoots: appConfig.allowedHostRoots, + dataRoot: appConfig.dataRoot, + database, + workspaceRoot: appConfig.workspaceRoot, + }); + const driver = new SbxDriver({ + binary: 'sbx', + sandboxPort: appConfig.sandboxPort, + template: appConfig.sandboxTemplate, + }); + const sandboxes = new SandboxService({ config: appConfig, database, driver, workspaces }); + const clients = new CodexProClientPool(sandboxes); + const tools = codexProToolManifest(); + const gateway = createGateway(appConfig, { + authProvider: new SingleUserAuthProvider(), + controlServer: { codexPro: clients, codexProTools: tools, sandboxes, workspaces }, + }); + let sandboxId: string | undefined; + const hostEscapeMarker = path.join(os.tmpdir(), `chat2shell-host-escape-${randomUUID()}`); + + try { + await driver.assertReady(); + const url = `http://127.0.0.1:${await listen(gateway)}/mcp`; + await rpc(url, 1, 'initialize', { + capabilities: {}, + clientInfo: { name: 'e2e', version: '1' }, + protocolVersion: '2025-06-18', + }); + + const listedTools = await rpc(url, 2, 'tools/list'); + const toolList = ( + listedTools.result as { tools: Array<{ inputSchema: { required?: string[] }; name: string }> } + ).tools; + expect(toolList.find((tool) => tool.name === 'bash')?.inputSchema.required).toContain( + 'sandbox_id', + ); + + const createResult = await callTool(url, 3, 'sandbox_create', {}); + const created = createResult.structuredContent as { + sandbox: { id: string; workspace: { id: string; root: string } }; + status: string; + }; + expect(created.status).toBe('created'); + sandboxId = created.sandbox.id; + const runtimeName = `c2s-${sandboxId.slice(4, 25)}`; + + const policy = spawnSync( + 'sbx', + ['policy', 'check', 'network', '--sandbox', runtimeName, 'openrouter.ai'], + { encoding: 'utf8' }, + ); + expect(policy.status).toBe(1); + expect( + policy.stdout, + 'chat2shell sandboxes must not use the inherited opencodex credential domain', + ).toMatch(/Denied/i); + + const write = await callTool(url, 4, 'write', { + content: 'isolated\n', + path: 'proof.txt', + sandbox_id: sandboxId, + }); + expect(write.isError).not.toBe(true); + expect((write.structuredContent as { workspace_id: string }).workspace_id).toBe( + created.sandbox.workspace.id, + ); + expect(fs.readFileSync(path.join(created.sandbox.workspace.root, 'proof.txt'), 'utf8')).toBe( + 'isolated\n', + ); + + const escape = await callTool(url, 5, 'bash', { + command: `touch ${hostEscapeMarker}`, + sandbox_id: sandboxId, + }); + expect(escape.isError).not.toBe(true); + expect(fs.existsSync(hostEscapeMarker), 'sandbox /tmp must not be the host /tmp').toBe(false); + + const docker = await callTool(url, 6, 'bash', { + command: "docker info --format '{{.ServerVersion}}'", + sandbox_id: sandboxId, + }); + expect(docker.isError, JSON.stringify(docker)).not.toBe(true); + + const longCommand = await callTool(url, 7, 'bash', { + command: 'sleep 35 && printf alive', + sandbox_id: sandboxId, + timeout_ms: 60_000, + }); + expect(longCommand.isError, JSON.stringify(longCommand)).not.toBe(true); + const afterLongCommand = await callTool(url, 8, 'bash', { + command: 'printf still-alive', + sandbox_id: sandboxId, + }); + expect(afterLongCommand.isError, JSON.stringify(afterLongCommand)).not.toBe(true); + + const preview = await callTool(url, 9, 'bash', { + command: + 'nohup node -e \'require("http").createServer((_request, response) => response.end("sandbox-preview")).listen(3000, "0.0.0.0")\' >/tmp/chat2shell-preview.log 2>&1 }).sandboxes[0]?.id, + ).toBe(sandboxId); + + const destroyed = await callTool(url, 13, 'sandbox_destroy', { sandbox_id: sandboxId }); + expect((destroyed.structuredContent as { status: string }).status).toBe('destroyed'); + sandboxId = undefined; + expect(workspaces.list('local-owner')[0]?.status).toBe('retained'); + + const hostRepository = path.join(allowedRoot, 'repository'); + execFileSync('git', ['clone', '--quiet', '--no-hardlinks', process.cwd(), hostRepository]); + const cloneWorkspace = workspaces.registerHost('local-owner', hostRepository, 'clone'); + const cloneCreateResult = await callTool(url, 14, 'sandbox_create', { + workspace_id: cloneWorkspace.id, + }); + const cloneCreated = cloneCreateResult.structuredContent as { + sandbox: { id: string }; + status: string; + }; + expect(cloneCreated.status).toBe('created'); + sandboxId = cloneCreated.sandbox.id; + + const cloneWrite = await callTool(url, 15, 'write', { + content: 'private clone\n', + path: 'clone-proof.txt', + sandbox_id: sandboxId, + }); + expect(cloneWrite.isError).not.toBe(true); + expect( + fs.existsSync(path.join(hostRepository, 'clone-proof.txt')), + 'clone mode must not modify the host checkout', + ).toBe(false); + + await callTool(url, 16, 'sandbox_destroy', { sandbox_id: sandboxId }); + sandboxId = undefined; + } finally { + if (sandboxId) { + await sandboxes.destroy('local-owner', sandboxId).catch(() => undefined); + } + await new Promise((resolve) => { + gateway.close(() => resolve()); + }); + await clients.closeAll(); + database.close(); + if (fs.existsSync(hostEscapeMarker)) { + fs.rmSync(hostEscapeMarker, { force: true }); + } + fs.rmSync(base, { force: true, recursive: true }); + } +}); diff --git a/test/gateway.test.ts b/test/gateway.test.ts deleted file mode 100644 index fc47690..0000000 --- a/test/gateway.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import assert from "node:assert/strict"; -import http from "node:http"; -import { once } from "node:events"; -import test from "node:test"; -import type { AppConfig } from "../src/config.js"; -import { SingleUserAuthProvider } from "../src/auth/single-user-provider.js"; -import { createGateway } from "../src/mcp/gateway.js"; - -async function listen(server: http.Server): Promise { - server.listen(0, "127.0.0.1"); - await once(server, "listening"); - const address = server.address(); - assert(address && typeof address === "object"); - return address.port; -} - -function config(): AppConfig { - return { - host: "127.0.0.1", port: 0, maxBodyBytes: 1024 * 1024, dataRoot: "/tmp/chat2shell", workspaceRoot: "/tmp/chat2shell/workspaces", - stateDir: "/tmp/chat2shell/state", databasePath: ":memory:", allowedHostRoots: ["/tmp"], sbxBinary: "sbx", - sandboxTemplate: "test:latest", sandboxPort: 18_787, - idleTimeoutMs: 1_000, workspaceRetentionMs: 10_000, reaperIntervalMs: 1_000, - }; -} - -async function rpc(url: string, id: number, method: string, params: Record = {}): Promise> { - const response = await fetch(url, { - method: "POST", - headers: { accept: "application/json, text/event-stream", "content-type": "application/json" }, - body: JSON.stringify({ jsonrpc: "2.0", id, method, params }), - }); - assert.equal(response.status, 200); - const text = await response.text(); - const data = text.split(/\r?\n/).find((line) => line.startsWith("data:")); - return JSON.parse(data ? data.slice(5).trim() : text) as Record; -} - -test("serves management tools itself instead of proxying to a host CodexPro", async (context) => { - let listCalls = 0; - let exposedPort: number | undefined; - const gateway = createGateway(config(), { - authProvider: new SingleUserAuthProvider(), - controlServer: { - sandboxes: { - async create() { throw new Error("not used"); }, - list() { listCalls += 1; return []; }, - get() { throw new Error("not used"); }, - async expose(_ownerId, sandboxId, port) { - exposedPort = port; - return { sandboxId, sandboxPort: port, hostPort: 32_000 }; - }, - async destroy() { throw new Error("not used"); }, - }, - workspaces: { list() { return []; } }, - codexPro: { async call() { throw new Error("not used"); } }, - codexProTools: [], - }, - }); - const port = await listen(gateway); - context.after(() => gateway.close()); - const url = `http://127.0.0.1:${port}/mcp`; - await rpc(url, 1, "initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "1" } }); - const listed = await rpc(url, 2, "tools/list"); - const tools = (listed.result as { tools: Array<{ name: string }> }).tools; - assert.deepEqual(tools.map((tool) => tool.name), ["sandbox_create", "sandbox_list", "sandbox_get", "sandbox_expose", "sandbox_destroy", "workspace_list"]); - const called = await rpc(url, 3, "tools/call", { name: "sandbox_list", arguments: {} }); - assert.equal((called.result as { structuredContent: { sandboxes: unknown[] } }).structuredContent.sandboxes.length, 0); - assert.equal(listCalls, 1); - const exposed = await rpc(url, 4, "tools/call", { name: "sandbox_expose", arguments: { sandbox_id: "sbx_test", port: 3_000 } }); - assert.deepEqual((exposed.result as { structuredContent: unknown }).structuredContent, { - sandboxId: "sbx_test", - sandboxPort: 3_000, - hostPort: 32_000, - }); - assert.equal(exposedPort, 3_000); -}); diff --git a/test/integration/gateway.test.ts b/test/integration/gateway.test.ts new file mode 100644 index 0000000..6eb233a --- /dev/null +++ b/test/integration/gateway.test.ts @@ -0,0 +1,230 @@ +import type http from 'node:http'; +import { once } from 'node:events'; +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { expect, onTestFinished, test } from 'vitest'; +import type { AppConfig } from '../../src/config.js'; +import { SingleUserAuthProvider } from '../../src/auth/single-user-provider.js'; +import { createGateway } from '../../src/mcp/gateway.js'; + +async function listen(server: http.Server): Promise { + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Expected a TCP server address'); + } + return address.port; +} + +function config(): AppConfig { + return { + allowedHostRoots: ['/tmp'], + dataRoot: '/tmp/chat2shell', + databasePath: ':memory:', + host: '127.0.0.1', + idleTimeoutMs: 1_000, + maxBodyBytes: 1024 * 1024, + port: 0, + reaperIntervalMs: 1_000, + sandboxPort: 18_787, + sandboxTemplate: 'test:latest', + sbxBinary: 'sbx', + stateDir: '/tmp/chat2shell/state', + workspaceRetentionMs: 10_000, + workspaceRoot: '/tmp/chat2shell/workspaces', + }; +} + +async function rpc( + url: string, + id: number, + method: string, + params: Record = {}, +): Promise> { + const response = await fetch(url, { + body: JSON.stringify({ id, jsonrpc: '2.0', method, params }), + headers: { + 'accept': 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + method: 'POST', + }); + expect(response.status).toBe(200); + const text = await response.text(); + const data = text.split(/\r?\n/).find((line) => line.startsWith('data:')); + return JSON.parse(data ? data.slice(5).trim() : text) as Record; +} + +test('serves management tools itself instead of proxying to a host CodexPro', async () => { + let listCalls = 0; + let exposedPort: number | undefined; + const gateway = createGateway(config(), { + authProvider: new SingleUserAuthProvider(), + controlServer: { + codexPro: { + call() { + return Promise.reject(new Error('not used')); + }, + }, + codexProTools: [], + sandboxes: { + create() { + return Promise.reject(new Error('not used')); + }, + destroy() { + return Promise.reject(new Error('not used')); + }, + expose(_ownerId, sandboxId, port) { + exposedPort = port; + return Promise.resolve({ hostPort: 32_000, sandboxId, sandboxPort: port }); + }, + get() { + throw new Error('not used'); + }, + list() { + listCalls += 1; + return []; + }, + }, + workspaces: { + list() { + return []; + }, + }, + }, + }); + onTestFinished( + () => + new Promise((resolve) => { + gateway.close(() => resolve()); + }), + ); + const port = await listen(gateway); + const url = `http://127.0.0.1:${port}/mcp`; + + await rpc(url, 1, 'initialize', { + capabilities: {}, + clientInfo: { name: 'test', version: '1' }, + protocolVersion: '2025-06-18', + }); + const listed = await rpc(url, 2, 'tools/list'); + const tools = (listed.result as { tools: Array<{ name: string }> }).tools; + expect(tools.map((tool) => tool.name)).toEqual([ + 'sandbox_create', + 'sandbox_list', + 'sandbox_get', + 'sandbox_expose', + 'sandbox_destroy', + 'workspace_list', + ]); + + const called = await rpc(url, 3, 'tools/call', { arguments: {}, name: 'sandbox_list' }); + expect( + (called.result as { structuredContent: { sandboxes: unknown[] } }).structuredContent.sandboxes, + ).toHaveLength(0); + expect(listCalls).toBe(1); + + const exposed = await rpc(url, 4, 'tools/call', { + arguments: { port: 3_000, sandbox_id: 'sbx_test' }, + name: 'sandbox_expose', + }); + expect((exposed.result as { structuredContent: unknown }).structuredContent).toEqual({ + hostPort: 32_000, + sandboxId: 'sbx_test', + sandboxPort: 3_000, + }); + expect(exposedPort).toBe(3_000); +}); + +test('routes CodexPro tools by sandbox_id without forwarding routing fields', async () => { + const calls: Array<{ + args: Record; + ownerId: string; + sandboxId: string; + toolName: string; + }> = []; + const bashTool: Tool = { + description: 'Run a command', + inputSchema: { + additionalProperties: false, + properties: { command: { type: 'string' } }, + required: ['command'], + type: 'object', + }, + name: 'bash', + }; + const gateway = createGateway(config(), { + authProvider: new SingleUserAuthProvider(), + controlServer: { + codexPro: { + call(ownerId, sandboxId, toolName, args) { + calls.push({ args, ownerId, sandboxId, toolName }); + return Promise.resolve({ content: [{ text: 'ok', type: 'text' }] }); + }, + }, + codexProTools: [bashTool], + sandboxes: { + create() { + return Promise.reject(new Error('not used')); + }, + destroy() { + return Promise.reject(new Error('not used')); + }, + expose() { + return Promise.reject(new Error('not used')); + }, + get() { + throw new Error('not used'); + }, + list() { + return []; + }, + }, + workspaces: { + list() { + return []; + }, + }, + }, + }); + onTestFinished( + () => + new Promise((resolve) => { + gateway.close(() => resolve()); + }), + ); + const port = await listen(gateway); + const url = `http://127.0.0.1:${port}/mcp`; + + await rpc(url, 1, 'initialize', { + capabilities: {}, + clientInfo: { name: 'test', version: '1' }, + protocolVersion: '2025-06-18', + }); + + const called = await rpc(url, 2, 'tools/call', { + arguments: { command: 'pwd', sandbox_id: 'sbx_test' }, + name: 'bash', + }); + expect((called.result as { isError?: boolean }).isError).not.toBe(true); + expect(calls).toEqual([ + { + args: { command: 'pwd' }, + ownerId: 'local-owner', + sandboxId: 'sbx_test', + toolName: 'bash', + }, + ]); + + const rejected = await rpc(url, 3, 'tools/call', { + arguments: { command: 'pwd', sandbox_id: 'sbx_test', workspace_id: 'ws_internal' }, + name: 'bash', + }); + const rejectedResult = rejected.result as { + content: Array<{ text?: string; type: string }>; + isError?: boolean; + }; + expect(rejectedResult.isError).toBe(true); + expect(rejectedResult.content[0]?.text).toMatch(/workspace_id is internal/); + expect(calls).toHaveLength(1); +}); diff --git a/test/integration/sandbox-service.test.ts b/test/integration/sandbox-service.test.ts new file mode 100644 index 0000000..08f55fd --- /dev/null +++ b/test/integration/sandbox-service.test.ts @@ -0,0 +1,249 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { expect, onTestFinished, test } from 'vitest'; +import type { AppConfig } from '../../src/config.js'; +import type { SandboxCreateResult, SandboxSummary } from '../../src/domain/types.js'; +import type { PublishedPort, RuntimeInfo, SandboxDriver } from '../../src/sandbox/sbx-driver.js'; +import { SandboxService } from '../../src/sandbox/service.js'; +import { StateDatabase } from '../../src/state/database.js'; +import { WorkspaceService } from '../../src/workspaces/service.js'; + +class FakeDriver implements SandboxDriver { + readonly runtimes = new Map(); + createCalls = 0; + healthy = true; + removeCalls = 0; + startCalls = 0; + + assertReady(): Promise { + return Promise.resolve(); + } + + create(name: string): Promise<{ endpoint: string; runtimeRoot: string }> { + this.createCalls += 1; + this.runtimes.set(name, { name, status: 'running' }); + return Promise.resolve({ endpoint: 'http://127.0.0.1:1234/mcp', runtimeRoot: '/workspace' }); + } + + expose(_name: string, sandboxPort: number): Promise { + return Promise.resolve({ hostPort: 32_000, sandboxPort }); + } + + isHealthy(): Promise { + return Promise.resolve(this.healthy); + } + + list(): Promise { + return Promise.resolve([...this.runtimes.values()]); + } + + remove(name: string): Promise { + this.removeCalls += 1; + this.runtimes.delete(name); + return Promise.resolve(); + } + + startCodexPro(): Promise { + this.startCalls += 1; + return Promise.resolve(); + } + + waitUntilHealthy(): Promise { + return Promise.resolve(); + } +} + +function config(base: string): AppConfig { + return { + allowedHostRoots: [path.join(base, 'allowed')], + dataRoot: path.join(base, 'data'), + databasePath: ':memory:', + host: '127.0.0.1', + idleTimeoutMs: 1_000, + maxBodyBytes: 1_024, + port: 0, + reaperIntervalMs: 100, + sandboxPort: 18_787, + sandboxTemplate: 'test:latest', + sbxBinary: 'sbx', + stateDir: path.join(base, 'state'), + workspaceRetentionMs: 7_000, + workspaceRoot: path.join(base, 'data', 'workspaces'), + }; +} + +function fixture( + prefix: string, + now?: () => number, +): { + appConfig: AppConfig; + base: string; + database: StateDatabase; + driver: FakeDriver; + service: SandboxService; + workspaces: WorkspaceService; +} { + const base = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + fs.mkdirSync(path.join(base, 'allowed')); + const database = new StateDatabase(':memory:'); + const appConfig = config(base); + const workspaces = new WorkspaceService({ + allowedHostRoots: appConfig.allowedHostRoots, + dataRoot: appConfig.dataRoot, + database, + workspaceRoot: appConfig.workspaceRoot, + }); + const driver = new FakeDriver(); + const service = new SandboxService({ config: appConfig, database, driver, now, workspaces }); + + onTestFinished(() => { + database.close(); + fs.rmSync(base, { force: true, recursive: true }); + }); + + return { appConfig, base, database, driver, service, workspaces }; +} + +function sandboxFrom(result: SandboxCreateResult): SandboxSummary { + if (!result.sandbox) { + throw new Error(`Expected sandbox result, received ${result.status}`); + } + return result.sandbox; +} + +test('explicit sandbox ids are reusable and one active sandbox is kept per workspace', async () => { + const { driver, service, workspaces } = fixture('chat2shell-sandbox-'); + + const firstResult = await service.create('owner', {}); + expect(firstResult.status).toBe('created'); + const first = sandboxFrom(firstResult); + + const secondResult = await service.create('owner', { workspaceId: first.workspace.id }); + expect(secondResult.status).toBe('reused'); + expect(sandboxFrom(secondResult).id).toBe(first.id); + expect(driver.createCalls).toBe(1); + await expect( + service.create('owner', { workspaceId: first.workspace.id, workspaceMode: 'clone' }), + ).rejects.toThrow(/does not match/); + + const destroyed = await service.destroy('owner', first.id); + expect(destroyed.status).toBe('destroyed'); + expect(driver.removeCalls).toBe(1); + expect(workspaces.list('owner')[0]?.status).toBe('retained'); +}); + +test('host workspace requests stop at approval_required', async () => { + const { base, driver, service } = fixture('chat2shell-approval-'); + const repository = path.join(base, 'allowed', 'repo'); + fs.mkdirSync(repository, { recursive: true }); + + const result = await service.create('owner', { + workspaceMode: 'direct', + workspacePath: repository, + }); + expect(result.status).toBe('approval_required'); + expect(result.approval?.id ?? '').toMatch(/^approval_/); + expect(driver.createCalls).toBe(0); +}); + +test('exposes a running sandbox port on an automatically assigned host port', async () => { + const { service } = fixture('chat2shell-expose-'); + const created = sandboxFrom(await service.create('owner', {})); + + await expect(service.expose('owner', created.id, 3_000)).resolves.toEqual({ + hostPort: 32_000, + sandboxId: created.id, + sandboxPort: 3_000, + }); + await expect(service.expose('owner', created.id, 0)).rejects.toThrow(/integer from 1 to 65535/); +}); + +test('an unavailable runtime becomes an explicit failed sandbox without automatic restart', async () => { + const { driver, service } = fixture('chat2shell-failed-'); + const created = sandboxFrom(await service.create('owner', {})); + driver.healthy = false; + + await expect(service.readyForTool('owner', created.id)).rejects.toThrow( + /destroy this sandbox and create a new one/, + ); + expect(driver.startCalls).toBe(1); + expect(service.list('owner')[0]?.status).toBe('failed'); +}); + +test('every completed tool call renews the idle deadline without an absolute lifetime', async () => { + let now = 1_000; + const { appConfig, service } = fixture('chat2shell-activity-', () => now); + const created = sandboxFrom(await service.create('owner', {})); + + for (let call = 0; call < 30; call += 1) { + now += 500; + await service.withReady('owner', created.id, () => Promise.resolve()); + } + expect(service.get('owner', created.id).expiresAt).toBe(now + appConfig.idleTimeoutMs); + + now += 500; + await expect( + service.withReady('owner', created.id, () => Promise.reject(new Error('tool failed'))), + ).rejects.toThrow(/tool failed/); + const afterFailure = service.get('owner', created.id); + expect(afterFailure.lastActivityAt).toBe(now); + expect(afterFailure.expiresAt).toBe(now + appConfig.idleTimeoutMs); +}); + +test('idle removal retains its managed workspace for the configured period', async () => { + let now = 1_000; + const { appConfig, service, workspaces } = fixture('chat2shell-expiry-', () => now); + const created = sandboxFrom(await service.create('owner', {})); + now += appConfig.idleTimeoutMs; + + const result = await service.reap(); + const workspace = workspaces.list('owner')[0]; + expect(result.destroyed).toEqual([created.id]); + expect(workspace?.status).toBe('retained'); + expect(workspace?.retainedUntil).toBe(now + appConfig.workspaceRetentionMs); +}); + +test('idle cleanup rechecks activity after an in-flight call', async () => { + let now = 1_000; + const { appConfig, service } = fixture('chat2shell-reaper-race-', () => now); + const created = sandboxFrom(await service.create('owner', {})); + let finishCall: (() => void) | undefined; + const inFlightCall = service.withReady('owner', created.id, async () => { + await new Promise((resolve) => { + finishCall = resolve; + }); + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + + now += appConfig.idleTimeoutMs; + const cleanup = service.reap(); + if (!finishCall) { + throw new Error('Expected the in-flight call to start'); + } + finishCall(); + await inFlightCall; + + expect((await cleanup).destroyed).toEqual([]); + expect(service.get('owner', created.id).status).toBe('running'); +}); + +test('a controller restart invalidates runtimes that the new controller does not own', async () => { + const { appConfig, database, driver, service, workspaces } = fixture('chat2shell-reconcile-'); + const created = sandboxFrom(await service.create('owner', {})); + expect(created.status).toBe('running'); + + const restartedController = new SandboxService({ + config: appConfig, + database, + driver, + workspaces, + }); + await restartedController.reconcile(); + + expect(driver.removeCalls).toBe(1); + expect(restartedController.list('owner')[0]?.status).toBe('failed'); + expect(restartedController.list('owner')[0]?.error ?? '').toMatch(/chat2shell restarted/); +}); diff --git a/test/integration/workspace-service.test.ts b/test/integration/workspace-service.test.ts new file mode 100644 index 0000000..593b6d2 --- /dev/null +++ b/test/integration/workspace-service.test.ts @@ -0,0 +1,73 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { expect, onTestFinished, test } from 'vitest'; +import { StateDatabase } from '../../src/state/database.js'; +import { WorkspaceService } from '../../src/workspaces/service.js'; + +function fixture(): { base: string; database: StateDatabase; service: WorkspaceService } { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-workspaces-')); + const allowedRoot = path.join(base, 'allowed'); + fs.mkdirSync(allowedRoot); + const database = new StateDatabase(':memory:'); + const service = new WorkspaceService({ + allowedHostRoots: [allowedRoot], + dataRoot: path.join(base, 'data'), + database, + now: () => 1_000, + workspaceRoot: path.join(base, 'data', 'workspaces'), + }); + onTestFinished(() => { + database.close(); + fs.rmSync(base, { force: true, recursive: true }); + }); + return { base, database, service }; +} + +test('managed workspaces have an independent stable id and private directory', () => { + const { service } = fixture(); + const workspace = service.createManaged('owner'); + + expect(workspace.id).toMatch(/^ws_/); + expect(path.basename(workspace.root)).toBe(workspace.id); + expect(fs.statSync(workspace.root).mode & 0o777).toBe(0o700); +}); + +test('a host path becomes only a pending approval until approved locally', () => { + const { base, service } = fixture(); + const repository = path.join(base, 'allowed', 'repo'); + fs.mkdirSync(repository); + + const request = service.requestHost('owner', repository, 'direct'); + expect(request.status).toBe('pending'); + expect('requestedPath' in request).toBe(true); + + const workspace = service.approve(request.id); + expect(workspace.kind).toBe('host'); + expect(workspace.mode).toBe('direct'); + expect(workspace.root).toBe(repository); +}); + +test('paths outside allow roots and protected paths are rejected', () => { + const { base, service } = fixture(); + const outside = path.join(base, 'outside'); + fs.mkdirSync(outside); + + expect(() => service.requestHost('owner', outside, 'clone')).toThrow(/allowed host root/); + + const protectedPath = path.join(base, 'allowed', '.ssh', 'repo'); + fs.mkdirSync(protectedPath, { recursive: true }); + expect(() => service.requestHost('owner', protectedPath, 'direct')).toThrow( + /protected directory/, + ); +}); + +test('retained managed workspaces can be attached to a new sandbox before trashing', () => { + const { service } = fixture(); + const workspace = service.createManaged('owner'); + service.retainManaged(workspace, 10_000); + + const restored = service.getApproved('owner', workspace.id); + expect(restored.status).toBe('approved'); + expect(restored.root).toBe(workspace.root); +}); diff --git a/test/sandbox-service.test.ts b/test/sandbox-service.test.ts deleted file mode 100644 index 7315f64..0000000 --- a/test/sandbox-service.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import type { AppConfig } from "../src/config.js"; -import type { PublishedPort, RuntimeInfo, SandboxDriver } from "../src/sandbox/sbx-driver.js"; -import { SandboxService } from "../src/sandbox/service.js"; -import { StateDatabase } from "../src/state/database.js"; -import { WorkspaceService } from "../src/workspaces/service.js"; - -class FakeDriver implements SandboxDriver { - readonly runtimes = new Map(); - createCalls = 0; - startCalls = 0; - removeCalls = 0; - healthy = true; - async assertReady(): Promise {} - async create(name: string): Promise<{ endpoint: string; runtimeRoot: string }> { - this.createCalls += 1; - this.runtimes.set(name, { name, status: "running" }); - return { endpoint: "http://127.0.0.1:1234/mcp", runtimeRoot: "/workspace" }; - } - async startCodexPro(): Promise { this.startCalls += 1; } - async waitUntilHealthy(): Promise {} - async isHealthy(): Promise { return this.healthy; } - async expose(_name: string, sandboxPort: number): Promise { return { sandboxPort, hostPort: 32_000 }; } - async remove(name: string): Promise { this.removeCalls += 1; this.runtimes.delete(name); } - async list(): Promise { return [...this.runtimes.values()]; } -} - -function config(base: string): AppConfig { - return { - host: "127.0.0.1", port: 0, maxBodyBytes: 1024, dataRoot: path.join(base, "data"), - workspaceRoot: path.join(base, "data", "workspaces"), stateDir: path.join(base, "state"), databasePath: ":memory:", - allowedHostRoots: [path.join(base, "allowed")], sbxBinary: "sbx", sandboxTemplate: "test:latest", - sandboxPort: 18_787, idleTimeoutMs: 1_000, - workspaceRetentionMs: 7_000, reaperIntervalMs: 100, - }; -} - -test("explicit sandbox ids are reusable and one active sandbox is kept per workspace", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-sandbox-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - const service = new SandboxService({ database, workspaces, driver, config: appConfig }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const first = await service.create("owner", {}); - assert.equal(first.status, "created"); - assert(first.sandbox); - const second = await service.create("owner", { workspaceId: first.sandbox.workspace.id }); - assert.equal(second.status, "reused"); - assert.equal(second.sandbox?.id, first.sandbox.id); - assert.equal(driver.createCalls, 1); - await assert.rejects(() => service.create("owner", { workspaceId: first.sandbox!.workspace.id, workspaceMode: "clone" }), /does not match/); - - const destroyed = await service.destroy("owner", first.sandbox.id); - assert.equal(destroyed.status, "destroyed"); - assert.equal(driver.removeCalls, 1); - assert.equal(workspaces.list("owner")[0]?.status, "retained"); -}); - -test("host workspace requests stop at approval_required", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-approval-")); - const repository = path.join(base, "allowed", "repo"); - fs.mkdirSync(repository, { recursive: true }); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - const service = new SandboxService({ database, workspaces, driver, config: appConfig }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - const result = await service.create("owner", { workspacePath: repository, workspaceMode: "direct" }); - assert.equal(result.status, "approval_required"); - assert.match(result.approval?.id ?? "", /^approval_/); - assert.equal(driver.createCalls, 0); -}); - -test("exposes a running sandbox port on an automatically assigned host port", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-expose-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const service = new SandboxService({ database, workspaces, driver: new FakeDriver(), config: appConfig }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const created = await service.create("owner", {}); - assert(created.sandbox); - assert.deepEqual(await service.expose("owner", created.sandbox.id, 3_000), { - sandboxId: created.sandbox.id, - sandboxPort: 3_000, - hostPort: 32_000, - }); - await assert.rejects(() => service.expose("owner", created.sandbox!.id, 0), /integer from 1 to 65535/); -}); - -test("an unavailable runtime becomes an explicit failed sandbox without automatic restart", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-failed-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - const service = new SandboxService({ database, workspaces, driver, config: appConfig }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const created = await service.create("owner", {}); - assert(created.sandbox); - driver.healthy = false; - - await assert.rejects(() => service.readyForTool("owner", created.sandbox!.id), /destroy this sandbox and create a new one/); - assert.equal(driver.startCalls, 1, "health failure must not start another CodexPro process"); - assert.equal(service.list("owner")[0]?.status, "failed"); -}); - -test("every completed tool call renews the idle deadline without an absolute lifetime", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-activity-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - let now = 1_000; - const service = new SandboxService({ database, workspaces, driver, config: appConfig, now: () => now }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const created = await service.create("owner", {}); - assert(created.sandbox); - const sandboxId = created.sandbox.id; - - for (let call = 0; call < 30; call += 1) { - now += 500; - await service.withReady("owner", sandboxId, async () => undefined); - } - assert.equal(service.get("owner", sandboxId).expiresAt, now + appConfig.idleTimeoutMs); - - now += 500; - await assert.rejects( - service.withReady("owner", sandboxId, async () => { throw new Error("tool failed"); }), - /tool failed/, - ); - const afterFailure = service.get("owner", sandboxId); - assert.equal(afterFailure.lastActivityAt, now); - assert.equal(afterFailure.expiresAt, now + appConfig.idleTimeoutMs); -}); - -test("idle removal retains its managed workspace for the configured period", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-expiry-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - let now = 1_000; - const service = new SandboxService({ database, workspaces, driver, config: appConfig, now: () => now }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const created = await service.create("owner", {}); - assert(created.sandbox); - now += appConfig.idleTimeoutMs; - - const result = await service.reap(); - const workspace = workspaces.list("owner")[0]; - assert.deepEqual(result.destroyed, [created.sandbox.id]); - assert.equal(workspace?.status, "retained"); - assert.equal(workspace?.retainedUntil, now + appConfig.workspaceRetentionMs); -}); - -test("idle cleanup rechecks activity after an in-flight call", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-reaper-race-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - let now = 1_000; - const service = new SandboxService({ database, workspaces, driver, config: appConfig, now: () => now }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const created = await service.create("owner", {}); - assert(created.sandbox); - let finishCall!: () => void; - const inFlightCall = service.withReady("owner", created.sandbox.id, async () => { - await new Promise((resolve) => { finishCall = resolve; }); - }); - await new Promise((resolve) => setImmediate(resolve)); - - now += appConfig.idleTimeoutMs; - const cleanup = service.reap(); - finishCall(); - await inFlightCall; - - assert.deepEqual((await cleanup).destroyed, []); - assert.equal(service.get("owner", created.sandbox.id).status, "running"); -}); - -test("a controller restart invalidates runtimes that the new controller does not own", async (context) => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-reconcile-")); - fs.mkdirSync(path.join(base, "allowed")); - const database = new StateDatabase(":memory:"); - const appConfig = config(base); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new FakeDriver(); - const firstController = new SandboxService({ database, workspaces, driver, config: appConfig }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - - const created = await firstController.create("owner", {}); - assert(created.sandbox); - const restartedController = new SandboxService({ database, workspaces, driver, config: appConfig }); - await restartedController.reconcile(); - - assert.equal(driver.removeCalls, 1); - assert.equal(restartedController.list("owner")[0]?.status, "failed"); - assert.match(restartedController.list("owner")[0]?.error ?? "", /chat2shell restarted/); -}); diff --git a/test/sbx-integration.test.ts b/test/sbx-integration.test.ts deleted file mode 100644 index 70a47c5..0000000 --- a/test/sbx-integration.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import assert from "node:assert/strict"; -import { randomUUID } from "node:crypto"; -import { execFileSync, spawnSync } from "node:child_process"; -import { once } from "node:events"; -import fs from "node:fs"; -import http from "node:http"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { SingleUserAuthProvider } from "../src/auth/single-user-provider.js"; -import { CodexProClientPool } from "../src/codexpro/client-pool.js"; -import { codexProToolManifest } from "../src/codexpro/tool-manifest.js"; -import type { AppConfig } from "../src/config.js"; -import { createGateway } from "../src/mcp/gateway.js"; -import { SbxDriver } from "../src/sandbox/sbx-driver.js"; -import { SandboxService } from "../src/sandbox/service.js"; -import { StateDatabase } from "../src/state/database.js"; -import { WorkspaceService } from "../src/workspaces/service.js"; - -const enabled = process.env.CHAT2SHELL_RUN_SBX_INTEGRATION === "1"; - -async function listen(server: http.Server): Promise { - server.listen(0, "127.0.0.1"); - await once(server, "listening"); - const address = server.address(); - assert(address && typeof address === "object"); - return address.port; -} - -async function rpc(url: string, id: number, method: string, params: Record = {}): Promise> { - const response = await fetch(url, { - method: "POST", - headers: { accept: "application/json, text/event-stream", "content-type": "application/json" }, - body: JSON.stringify({ jsonrpc: "2.0", id, method, params }), - }); - const text = await response.text(); - assert.equal(response.status, 200, text); - const data = text.split(/\r?\n/).find((line) => line.startsWith("data:")); - return JSON.parse(data ? data.slice(5).trim() : text) as Record; -} - -async function callTool(url: string, id: number, name: string, args: Record): Promise> { - const response = await rpc(url, id, "tools/call", { name, arguments: args }); - return response.result as Record; -} - -test("the public MCP boundary routes full shell and private Docker only into a real microVM", { skip: !enabled }, async () => { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-integration-")); - const allowedRoot = path.join(base, "host"); - fs.mkdirSync(allowedRoot); - const appConfig: AppConfig = { - host: "127.0.0.1", port: 0, maxBodyBytes: 20 * 1024 * 1024, - dataRoot: path.join(base, "data"), workspaceRoot: path.join(base, "data", "workspaces"), - stateDir: path.join(base, "data", "state"), databasePath: path.join(base, "data", "state", "test.sqlite"), - allowedHostRoots: [allowedRoot], sbxBinary: "sbx", sandboxTemplate: "chat2shell-codexpro:0.30.0", - sandboxPort: 18_787, idleTimeoutMs: 24 * 60 * 60_000, - workspaceRetentionMs: 30 * 24 * 60 * 60_000, - reaperIntervalMs: 60_000, - }; - const database = new StateDatabase(appConfig.databasePath); - const workspaces = new WorkspaceService({ database, dataRoot: appConfig.dataRoot, workspaceRoot: appConfig.workspaceRoot, allowedHostRoots: appConfig.allowedHostRoots }); - const driver = new SbxDriver({ binary: "sbx", template: appConfig.sandboxTemplate, sandboxPort: appConfig.sandboxPort }); - const sandboxes = new SandboxService({ database, workspaces, driver, config: appConfig }); - const clients = new CodexProClientPool(sandboxes); - const tools = codexProToolManifest(); - const gateway = createGateway(appConfig, { - authProvider: new SingleUserAuthProvider(), - controlServer: { sandboxes, workspaces, codexPro: clients, codexProTools: tools }, - }); - let sandboxId: string | undefined; - const hostEscapeMarker = path.join(os.tmpdir(), `chat2shell-host-escape-${randomUUID()}`); - - try { - await driver.assertReady(); - const url = `http://127.0.0.1:${await listen(gateway)}/mcp`; - await rpc(url, 1, "initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "integration", version: "1" } }); - const listedTools = await rpc(url, 2, "tools/list"); - const toolList = (listedTools.result as { tools: Array<{ name: string; inputSchema: { required?: string[] } }> }).tools; - assert(toolList.find((tool) => tool.name === "bash")?.inputSchema.required?.includes("sandbox_id")); - - const createResult = await callTool(url, 3, "sandbox_create", {}); - const created = createResult.structuredContent as { status: string; sandbox: { id: string; workspace: { id: string; root: string } } }; - assert.equal(created.status, "created"); - sandboxId = created.sandbox.id; - const runtimeName = `c2s-${sandboxId.slice(4, 25)}`; - const policy = spawnSync("sbx", ["policy", "check", "network", "--sandbox", runtimeName, "openrouter.ai"], { encoding: "utf8" }); - assert.equal(policy.status, 1); - assert.match(policy.stdout, /Denied/i, "chat2shell sandboxes must not use the inherited opencodex credential domain"); - - const write = await callTool(url, 4, "write", { sandbox_id: sandboxId, path: "proof.txt", content: "isolated\n" }); - assert.notEqual(write.isError, true); - assert.equal((write.structuredContent as { workspace_id: string }).workspace_id, created.sandbox.workspace.id); - assert.equal(fs.readFileSync(path.join(created.sandbox.workspace.root, "proof.txt"), "utf8"), "isolated\n"); - - const escape = await callTool(url, 5, "bash", { sandbox_id: sandboxId, command: `touch ${hostEscapeMarker}` }); - assert.notEqual(escape.isError, true); - assert.equal(fs.existsSync(hostEscapeMarker), false, "sandbox /tmp must not be the host /tmp"); - - const docker = await callTool(url, 6, "bash", { sandbox_id: sandboxId, command: "docker info --format '{{.ServerVersion}}'" }); - assert.notEqual(docker.isError, true, JSON.stringify(docker)); - - const longCommand = await callTool(url, 7, "bash", { sandbox_id: sandboxId, command: "sleep 35 && printf alive", timeout_ms: 60_000 }); - assert.notEqual(longCommand.isError, true, JSON.stringify(longCommand)); - const afterLongCommand = await callTool(url, 8, "bash", { sandbox_id: sandboxId, command: "printf still-alive" }); - assert.notEqual(afterLongCommand.isError, true, JSON.stringify(afterLongCommand)); - - const preview = await callTool(url, 9, "bash", { - sandbox_id: sandboxId, - command: "nohup node -e 'require(\"http\").createServer((_request, response) => response.end(\"sandbox-preview\")).listen(3000, \"0.0.0.0\")' >/tmp/chat2shell-preview.log 2>&1 }).sandboxes[0]?.id, sandboxId); - const destroyed = await callTool(url, 13, "sandbox_destroy", { sandbox_id: sandboxId }); - assert.equal((destroyed.structuredContent as { status: string }).status, "destroyed"); - sandboxId = undefined; - assert.equal(workspaces.list("local-owner")[0]?.status, "retained"); - - const hostRepository = path.join(allowedRoot, "repository"); - execFileSync("git", ["clone", "--quiet", "--no-hardlinks", process.cwd(), hostRepository]); - const cloneWorkspace = workspaces.registerHost("local-owner", hostRepository, "clone"); - const cloneCreateResult = await callTool(url, 14, "sandbox_create", { workspace_id: cloneWorkspace.id }); - const cloneCreated = cloneCreateResult.structuredContent as { status: string; sandbox: { id: string } }; - assert.equal(cloneCreated.status, "created"); - sandboxId = cloneCreated.sandbox.id; - const cloneWrite = await callTool(url, 15, "write", { sandbox_id: sandboxId, path: "clone-proof.txt", content: "private clone\n" }); - assert.notEqual(cloneWrite.isError, true); - assert.equal(fs.existsSync(path.join(hostRepository, "clone-proof.txt")), false, "clone mode must not modify the host checkout"); - await callTool(url, 16, "sandbox_destroy", { sandbox_id: sandboxId }); - sandboxId = undefined; - } finally { - if (sandboxId) await sandboxes.destroy("local-owner", sandboxId).catch(() => undefined); - await new Promise((resolve) => gateway.close(() => resolve())); - await clients.closeAll(); - database.close(); - if (fs.existsSync(hostEscapeMarker)) fs.rmSync(hostEscapeMarker, { force: true }); - fs.rmSync(base, { recursive: true, force: true }); - } -}); diff --git a/test/tool-manifest.test.ts b/test/tool-manifest.test.ts deleted file mode 100644 index 33d9143..0000000 --- a/test/tool-manifest.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { codexProToolManifest, scopedCodexProTool } from "../src/codexpro/tool-manifest.js"; - -test("every CodexPro tool schema is static and scoped by an explicit sandbox_id", () => { - const tools = codexProToolManifest(); - assert(tools.length > 10); - for (const rawTool of tools) { - assert.equal(rawTool.inputSchema.properties?.workspace_id, undefined, rawTool.name); - const tool = scopedCodexProTool(rawTool); - assert.ok(tool.inputSchema.required?.includes("sandbox_id"), tool.name); - assert.equal((tool.inputSchema.properties?.sandbox_id as { type?: string }).type, "string", tool.name); - } - assert.equal(tools.some((tool) => tool.name === "open_workspace"), false); - assert.match(tools.find((tool) => tool.name === "bash")?.description ?? "", /unrestricted Bash/); -}); diff --git a/test/unit/client-pool.test.ts b/test/unit/client-pool.test.ts new file mode 100644 index 0000000..276de46 --- /dev/null +++ b/test/unit/client-pool.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from 'vitest'; +import { normalizeWorkspaceIdentity } from '../../src/codexpro/client-pool.js'; + +test("replaces CodexPro's internal workspace identity with the public chat2shell identity", () => { + const result = normalizeWorkspaceIdentity( + { + content: [{ type: 'text', text: 'Workspace ws_internal selected; ws_internal is ready.' }], + structuredContent: { + root: '/workspace', + selected_workspace_id: 'ws_internal', + workspace_id: 'ws_internal', + }, + }, + 'ws_public', + ); + + expect(result.structuredContent).toEqual({ + root: '/workspace', + selected_workspace_id: 'ws_public', + workspace_id: 'ws_public', + }); + expect(result.content[0]?.type).toBe('text'); + expect(result.content[0]?.type === 'text' ? result.content[0].text : undefined).toBe( + 'Workspace ws_public selected; ws_public is ready.', + ); +}); + +test('leaves results without a CodexPro workspace identity untouched', () => { + const result = { + content: [{ type: 'text' as const, text: 'No workspace identity' }], + structuredContent: { status: 'ok' }, + }; + + expect(normalizeWorkspaceIdentity(result, 'ws_public')).toBe(result); +}); diff --git a/test/unit/compatibility.test.ts b/test/unit/compatibility.test.ts new file mode 100644 index 0000000..c178bb4 --- /dev/null +++ b/test/unit/compatibility.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from 'vitest'; +import { sessionlessDiscoverResponse } from '../../src/mcp/compatibility.js'; + +test('returns method-not-found for a sessionless server/discover probe', () => { + const response = sessionlessDiscoverResponse( + {}, + Buffer.from('{"jsonrpc":"2.0","id":"probe","method":"server/discover","params":{}}'), + ); + + expect(response).toEqual({ + error: { code: -32601, message: 'Method not found' }, + id: 'probe', + jsonrpc: '2.0', + }); +}); + +test('does not intercept established MCP sessions', () => { + const response = sessionlessDiscoverResponse( + { 'mcp-session-id': 'session-1' }, + Buffer.from('{"jsonrpc":"2.0","id":1,"method":"server/discover"}'), + ); + + expect(response).toBeUndefined(); +}); diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts new file mode 100644 index 0000000..cd7e638 --- /dev/null +++ b/test/unit/config.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest'; +import { loadAppConfig } from '../../src/config.js'; + +test('uses the complete public lifecycle policy', () => { + const config = loadAppConfig({ HOME: '/tmp/chat2shell-config-test' }); + + expect(config.idleTimeoutMs).toBe(24 * 60 * 60_000); + expect(config.workspaceRetentionMs).toBe(30 * 24 * 60 * 60_000); + expect('maxLifetimeMs' in config).toBe(false); + expect('sandboxCpus' in config).toBe(false); + expect('sandboxMemory' in config).toBe(false); +}); diff --git a/test/unit/tool-manifest.test.ts b/test/unit/tool-manifest.test.ts new file mode 100644 index 0000000..436b62b --- /dev/null +++ b/test/unit/tool-manifest.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest'; +import { codexProToolManifest, scopedCodexProTool } from '../../src/codexpro/tool-manifest.js'; + +test('every CodexPro tool schema is static and scoped by an explicit sandbox_id', () => { + const tools = codexProToolManifest(); + + expect(tools.length).toBeGreaterThan(10); + for (const rawTool of tools) { + expect(rawTool.inputSchema.properties?.workspace_id, rawTool.name).toBeUndefined(); + const tool = scopedCodexProTool(rawTool); + expect(tool.inputSchema.required, tool.name).toContain('sandbox_id'); + expect(tool.inputSchema.properties?.sandbox_id, tool.name).toMatchObject({ type: 'string' }); + } + expect(tools.some((tool) => tool.name === 'open_workspace')).toBe(false); + expect(tools.find((tool) => tool.name === 'bash')?.description ?? '').toMatch( + /unrestricted Bash/, + ); +}); diff --git a/test/workspace-service.test.ts b/test/workspace-service.test.ts deleted file mode 100644 index fdb2ee8..0000000 --- a/test/workspace-service.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { StateDatabase } from "../src/state/database.js"; -import { WorkspaceService } from "../src/workspaces/service.js"; - -function fixture(context: test.TestContext): { base: string; database: StateDatabase; service: WorkspaceService } { - const base = fs.mkdtempSync(path.join(os.tmpdir(), "chat2shell-workspaces-")); - const allowedRoot = path.join(base, "allowed"); - fs.mkdirSync(allowedRoot); - const database = new StateDatabase(":memory:"); - const service = new WorkspaceService({ - database, - dataRoot: path.join(base, "data"), - workspaceRoot: path.join(base, "data", "workspaces"), - allowedHostRoots: [allowedRoot], - now: () => 1_000, - }); - context.after(() => { database.close(); fs.rmSync(base, { recursive: true, force: true }); }); - return { base, database, service }; -} - -test("managed workspaces have an independent stable id and private directory", (context) => { - const { service } = fixture(context); - const workspace = service.createManaged("owner"); - assert.match(workspace.id, /^ws_/); - assert.equal(path.basename(workspace.root), workspace.id); - assert.equal(fs.statSync(workspace.root).mode & 0o777, 0o700); -}); - -test("a host path becomes only a pending approval until approved locally", (context) => { - const { base, service } = fixture(context); - const repository = path.join(base, "allowed", "repo"); - fs.mkdirSync(repository); - const request = service.requestHost("owner", repository, "direct"); - assert.equal(request.status, "pending"); - assert.ok("requestedPath" in request); - const workspace = service.approve(request.id); - assert.equal(workspace.kind, "host"); - assert.equal(workspace.mode, "direct"); - assert.equal(workspace.root, repository); -}); - -test("paths outside allow roots and protected paths are rejected", (context) => { - const { base, service } = fixture(context); - const outside = path.join(base, "outside"); - fs.mkdirSync(outside); - assert.throws(() => service.requestHost("owner", outside, "clone"), /allowed host root/); - const protectedPath = path.join(base, "allowed", ".ssh", "repo"); - fs.mkdirSync(protectedPath, { recursive: true }); - assert.throws(() => service.requestHost("owner", protectedPath, "direct"), /protected directory/); -}); - -test("retained managed workspaces can be attached to a new sandbox before trashing", (context) => { - const { service } = fixture(context); - const workspace = service.createManaged("owner"); - service.retainManaged(workspace, 10_000); - const restored = service.getApproved("owner", workspace.id); - assert.equal(restored.status, "approved"); - assert.equal(restored.root, workspace.root); -}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..36a5ad7 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true + }, + "include": ["src/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 545dedd..91d4a97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "declaration": true, + "declaration": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "module": "NodeNext", @@ -13,5 +13,11 @@ "strict": true, "target": "ES2023" }, - "include": ["src/**/*.ts", "test/**/*.ts"] + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "vitest.config.ts", + "oxlint.config.ts", + "oxfmt.config.ts" + ] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7c6291d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + include: ['src/**/*.ts'], + provider: 'v8', + reporter: ['text'], + }, + projects: [ + { + test: { + environment: 'node', + include: ['test/unit/**/*.test.ts'], + name: 'unit', + }, + }, + { + test: { + environment: 'node', + include: ['test/integration/**/*.test.ts'], + name: 'integration', + }, + }, + { + test: { + environment: 'node', + fileParallelism: false, + hookTimeout: 30_000, + include: ['test/e2e/**/*.test.ts'], + name: 'e2e', + testTimeout: 600_000, + }, + }, + ], + }, +}); From 3f43290cc966d76b142ccdb1480805415e3dc255 Mon Sep 17 00:00:00 2001 From: retn0 Date: Thu, 3 Sep 2026 11:40:50 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=91=B7=20Add=20code=20quality=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..774ca39 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + name: Quality checks + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + version: 11.23.0 + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check formatting + run: pnpm format:check + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build From 484ec8a3c398c95540c856950c1ba2bf8e827032 Mon Sep 17 00:00:00 2001 From: retn0 Date: Thu, 3 Sep 2026 12:32:24 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20Bash=20sessions?= =?UTF-8?q?=20after=20snapshot=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/architecture.md | 2 +- src/codexpro/bash-sessions.ts | 26 ++++++++- src/codexpro/tool-manifest.ts | 3 +- test/integration/bash-sessions.test.ts | 77 ++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5ad9fb2..2e32132 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Every tool call that reaches a running sandbox counts as activity, whether it su Cleanup checks run once per minute. Sandbox resources use Docker Sandboxes defaults. The outer MCP server accepts request bodies up to 20 MiB. -Bash has no execution timeout unless `timeout_ms` is explicitly provided. `bash` always returns a `session_id` and waits up to `yield_time_ms`, which defaults to 10 seconds and accepts at most 60 seconds. `bash_poll` waits for new output, process exit, or its own `yield_time_ms` expiry; that wait also defaults to 10 seconds and accepts at most 60 seconds. It returns only new combined stdout/stderr. Poll again while `status` is `running` or `has_more_output` is true. `bash_stop` sends SIGTERM followed by SIGKILL after 1.5 seconds if necessary. chat2shell does not redact Bash output: everything printed inside the sandbox is visible to the MCP client. Sensitive data must be controlled by the files and credentials explicitly made available to the sandbox. Bash sessions exist only in their sandbox and disappear when that sandbox is removed. They are not recovered after a chat2shell restart, because restart reconciliation removes the old sandbox. +Bash has no execution timeout unless `timeout_ms` is explicitly provided. `bash` always returns a `session_id` and waits up to `yield_time_ms`, which defaults to 10 seconds and accepts at most 60 seconds. If command launch succeeds but the initial status/output snapshot cannot be read, `bash` preserves the session and conservatively returns `status: running` with no output so the caller can recover with `bash_poll`. `bash_poll` waits for new output, process exit, or its own `yield_time_ms` expiry; that wait also defaults to 10 seconds and accepts at most 60 seconds. It returns only new combined stdout/stderr. Poll again while `status` is `running` or `has_more_output` is true. `bash_stop` sends SIGTERM followed by SIGKILL after 1.5 seconds if necessary. chat2shell does not redact Bash output: everything printed inside the sandbox is visible to the MCP client. Sensitive data must be controlled by the files and credentials explicitly made available to the sandbox. Bash sessions exist only in their sandbox and disappear when that sandbox is removed. They are not recovered after a chat2shell restart, because restart reconciliation removes the old sandbox. If CodexPro becomes unavailable, the sandbox changes to `failed`. `sandbox_list` shows it, and the user must destroy it before creating a replacement. chat2shell does not guess how to recover it. diff --git a/docs/architecture.md b/docs/architecture.md index 8224b91..6be3e4e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -95,7 +95,7 @@ CodexPro assigns its own path-derived workspace ID inside the microVM. That inte CodexPro remains the only Bash executor. chat2shell starts each command through CodexPro as a detached process group inside the selected microVM, with combined stdout/stderr written to that microVM's `/tmp` directory. -`bash` always returns a random `session_id` and waits for completion for 10 seconds by default. `yield_time_ms` can explicitly change that wait from 0 to 60 seconds. If the command exits, the call returns `status: exited`, its output, and its exit code; otherwise it returns the output so far and `status: running`. This wait controls only when MCP yields a response and never kills the command. +`bash` always returns a random `session_id` and waits for completion for 10 seconds by default. `yield_time_ms` can explicitly change that wait from 0 to 60 seconds. If the command exits, the call returns `status: exited`, its output, and its exit code; otherwise it returns the output so far and `status: running`. Once the detached launch has succeeded, chat2shell keeps the session handle even if the first status/output snapshot fails. In that case `bash` conservatively returns `status: running`, empty output, and the same `session_id`; a later `bash_poll` recovers the actual state and unread output. This wait controls only when MCP yields a response and never kills the command. There is no command lifetime limit unless `timeout_ms` is explicitly supplied. `bash_poll` waits until new output appears, the process exits, or its `yield_time_ms` expires. Its wait defaults to 10 seconds and accepts at most 60 seconds. It returns only output not returned by earlier calls and reports the current status and exit code. Polls for one session are serialized. Each response reads at most 60,000 new bytes, preserves complete UTF-8 characters across reads, and reports `has_more_output` when already-buffered output remains. Call it again while `status` is `running` or `has_more_output` is true. `bash_stop` terminates the process group with SIGTERM and escalates to SIGKILL after 1.5 seconds. diff --git a/src/codexpro/bash-sessions.ts b/src/codexpro/bash-sessions.ts index 3dccbb4..75229a2 100644 --- a/src/codexpro/bash-sessions.ts +++ b/src/codexpro/bash-sessions.ts @@ -204,11 +204,16 @@ export class BashSessionService { timeout_ms: Math.max(5_000, yieldTimeMs + 5_000), }), ); - return await this.#snapshot(session); } catch (error) { this.#sessions.delete(id); throw error; } + + try { + return await this.#snapshot(session); + } catch { + return this.#unobservedStart(session); + } } async poll( @@ -251,6 +256,25 @@ export class BashSessionService { } } + #unobservedStart(session: BashSession): CallToolResult { + const structuredContent = { + session_id: session.id, + status: 'running', + exit_code: null, + output: '', + has_more_output: false, + }; + return { + content: [ + { + type: 'text', + text: `Bash session ${session.id} started, but its initial snapshot was unavailable. Poll it with bash_poll.`, + }, + ], + structuredContent, + }; + } + async #snapshot(session: BashSession, yieldTimeMs = 0): Promise { const result = session.snapshotQueue.then(() => this.#readSnapshot(session, yieldTimeMs)); session.snapshotQueue = result.then( diff --git a/src/codexpro/tool-manifest.ts b/src/codexpro/tool-manifest.ts index 799c257..a42cc87 100644 --- a/src/codexpro/tool-manifest.ts +++ b/src/codexpro/tool-manifest.ts @@ -8,7 +8,8 @@ const bashOutputSchema = { status: { type: 'string' as const, enum: ['running', 'exited'], - description: 'Whether the process is still running or has exited.', + description: + 'Last observed process state. If launch succeeds but the initial snapshot is unavailable, bash conservatively returns running so the session can be polled.', }, exit_code: { anyOf: [{ type: 'integer' as const }, { type: 'null' as const }], diff --git a/test/integration/bash-sessions.test.ts b/test/integration/bash-sessions.test.ts index f60207f..221ad9d 100644 --- a/test/integration/bash-sessions.test.ts +++ b/test/integration/bash-sessions.test.ts @@ -34,6 +34,30 @@ class LocalBashExecutor { } } +class FailOneCallExecutor { + readonly executor: LocalBashExecutor; + readonly failOnCall: number; + calls = 0; + + constructor(cwd: string, failOnCall: number) { + this.executor = new LocalBashExecutor(cwd); + this.failOnCall = failOnCall; + } + + call( + ownerId: string, + sandboxId: string, + toolName: string, + args: Record, + ): Promise { + this.calls += 1; + if (this.calls === this.failOnCall) { + return Promise.reject(new Error(`injected executor failure ${this.calls}`)); + } + return this.executor.call(ownerId, sandboxId, toolName, args); + } +} + class SerializedBashExecutor { readonly executor: LocalBashExecutor; queue = Promise.resolve(); @@ -69,6 +93,14 @@ function cleanupSession(id: string): void { fs.rmSync(`/tmp/chat2shell-bash/${id}`, { recursive: true, force: true }); } +function sessionOutput(result: CallToolResult): string { + const value = result.structuredContent?.output; + if (typeof value !== 'string') { + throw new Error('Missing Bash session output'); + } + return value; +} + async function waitForSessionOutput(id: string, expected: string): Promise { const outputPath = `/tmp/chat2shell-bash/${id}/output.log`; for (let attempt = 0; attempt < 100; attempt += 1) { @@ -103,6 +135,51 @@ test('returns exited output for a short command', async () => { }); }); +test('preserves the session handle when the initial snapshot fails', async () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-bash-test-')); + onTestFinished(() => fs.rmSync(cwd, { recursive: true, force: true })); + const executor = new FailOneCallExecutor(cwd, 2); + const sessions = new BashSessionService(executor); + + const started = await sessions.start('owner', 'sandbox', { + command: 'printf recovered', + yieldTimeMs: 0, + }); + const id = sessionId(started); + onTestFinished(() => cleanupSession(id)); + + expect(started.isError).not.toBe(true); + expect(started.structuredContent).toEqual({ + session_id: id, + status: 'running', + exit_code: null, + output: '', + has_more_output: false, + }); + const startText = started.content.find((item) => item.type === 'text')?.text; + expect(startText).toMatch(/initial snapshot was unavailable.*bash_poll/i); + + let observed = await sessions.poll('owner', 'sandbox', id, { yieldTimeMs: 1_000 }); + let output = sessionOutput(observed); + if (observed.structuredContent?.status === 'running') { + observed = await sessions.poll('owner', 'sandbox', id, { yieldTimeMs: 1_000 }); + output += sessionOutput(observed); + } + + expect(observed.structuredContent?.status).toBe('exited'); + expect(output).toBe('recovered'); +}); + +test('still rejects when the launch itself fails', async () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-bash-test-')); + onTestFinished(() => fs.rmSync(cwd, { recursive: true, force: true })); + const sessions = new BashSessionService(new FailOneCallExecutor(cwd, 1)); + + await expect( + sessions.start('owner', 'sandbox', { command: 'printf never-started', yieldTimeMs: 0 }), + ).rejects.toThrow(/injected executor failure 1/); +}); + test('returns a running session and long-polls for only new output', async () => { const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-bash-test-')); onTestFinished(() => fs.rmSync(cwd, { recursive: true, force: true })); From 5e52df12d8d87385e57b0197113fa39eb27bcfa9 Mon Sep 17 00:00:00 2001 From: retn0 Date: Thu, 3 Sep 2026 12:53:52 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20Reject=20stale=20Bash=20sess?= =?UTF-8?q?ions=20after=20sandbox=20destruction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/codexpro/bash-sessions.ts | 5 ++++- test/integration/bash-sessions.test.ts | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/codexpro/bash-sessions.ts b/src/codexpro/bash-sessions.ts index 75229a2..5bb7f90 100644 --- a/src/codexpro/bash-sessions.ts +++ b/src/codexpro/bash-sessions.ts @@ -211,7 +211,10 @@ export class BashSessionService { try { return await this.#snapshot(session); - } catch { + } catch (error) { + if (this.#sessions.get(id) !== session) { + throw error; + } return this.#unobservedStart(session); } } diff --git a/test/integration/bash-sessions.test.ts b/test/integration/bash-sessions.test.ts index 221ad9d..fbc7ae9 100644 --- a/test/integration/bash-sessions.test.ts +++ b/test/integration/bash-sessions.test.ts @@ -37,11 +37,13 @@ class LocalBashExecutor { class FailOneCallExecutor { readonly executor: LocalBashExecutor; readonly failOnCall: number; + readonly beforeFailure?: () => void; calls = 0; - constructor(cwd: string, failOnCall: number) { + constructor(cwd: string, failOnCall: number, beforeFailure?: () => void) { this.executor = new LocalBashExecutor(cwd); this.failOnCall = failOnCall; + this.beforeFailure = beforeFailure; } call( @@ -52,6 +54,7 @@ class FailOneCallExecutor { ): Promise { this.calls += 1; if (this.calls === this.failOnCall) { + this.beforeFailure?.(); return Promise.reject(new Error(`injected executor failure ${this.calls}`)); } return this.executor.call(ownerId, sandboxId, toolName, args); @@ -170,6 +173,20 @@ test('preserves the session handle when the initial snapshot fails', async () => expect(output).toBe('recovered'); }); +test('does not return a stale handle after concurrent sandbox destruction', async () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-bash-test-')); + onTestFinished(() => fs.rmSync(cwd, { recursive: true, force: true })); + let destroyListener: ((sandboxId: string) => void) | undefined; + const executor = new FailOneCallExecutor(cwd, 2, () => destroyListener?.('sandbox')); + const sessions = new BashSessionService(executor, (listener) => { + destroyListener = listener; + }); + + await expect( + sessions.start('owner', 'sandbox', { command: 'sleep 30', yieldTimeMs: 0 }), + ).rejects.toThrow(/injected executor failure 2/); +}); + test('still rejects when the launch itself fails', async () => { const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-bash-test-')); onTestFinished(() => fs.rmSync(cwd, { recursive: true, force: true })); From 76015bd46b1016c51d3bfe4e10712f2d85bb9eba Mon Sep 17 00:00:00 2001 From: retn0 Date: Thu, 3 Sep 2026 14:04:32 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=85=20Avoid=20process=20leaks=20in=20?= =?UTF-8?q?Bash=20race=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/bash-sessions.test.ts | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/test/integration/bash-sessions.test.ts b/test/integration/bash-sessions.test.ts index fbc7ae9..5ec9820 100644 --- a/test/integration/bash-sessions.test.ts +++ b/test/integration/bash-sessions.test.ts @@ -37,13 +37,11 @@ class LocalBashExecutor { class FailOneCallExecutor { readonly executor: LocalBashExecutor; readonly failOnCall: number; - readonly beforeFailure?: () => void; calls = 0; - constructor(cwd: string, failOnCall: number, beforeFailure?: () => void) { + constructor(cwd: string, failOnCall: number) { this.executor = new LocalBashExecutor(cwd); this.failOnCall = failOnCall; - this.beforeFailure = beforeFailure; } call( @@ -54,7 +52,6 @@ class FailOneCallExecutor { ): Promise { this.calls += 1; if (this.calls === this.failOnCall) { - this.beforeFailure?.(); return Promise.reject(new Error(`injected executor failure ${this.calls}`)); } return this.executor.call(ownerId, sandboxId, toolName, args); @@ -174,17 +171,30 @@ test('preserves the session handle when the initial snapshot fails', async () => }); test('does not return a stale handle after concurrent sandbox destruction', async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chat2shell-bash-test-')); - onTestFinished(() => fs.rmSync(cwd, { recursive: true, force: true })); let destroyListener: ((sandboxId: string) => void) | undefined; - const executor = new FailOneCallExecutor(cwd, 2, () => destroyListener?.('sandbox')); + let calls = 0; + const executor = { + call(_ownerId: string, sandboxId: string, toolName: string): Promise { + expect(toolName).toBe('bash'); + calls += 1; + if (calls === 1) { + return Promise.resolve({ + content: [{ type: 'text', text: '' }], + structuredContent: { exitCode: 0, stdout: '', stderr: '' }, + }); + } + destroyListener?.(sandboxId); + return Promise.reject(new Error('injected snapshot failure after destruction')); + }, + }; const sessions = new BashSessionService(executor, (listener) => { destroyListener = listener; }); await expect( - sessions.start('owner', 'sandbox', { command: 'sleep 30', yieldTimeMs: 0 }), - ).rejects.toThrow(/injected executor failure 2/); + sessions.start('owner', 'sandbox', { command: 'ignored by fake executor', yieldTimeMs: 0 }), + ).rejects.toThrow(/snapshot failure after destruction/); + expect(calls).toBe(2); }); test('still rejects when the launch itself fails', async () => {