commit 8454476c93b264f3ceb9cdf603372c929ad057b8 Author: kst Date: Wed Aug 19 22:52:46 2026 +0900 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8cfbb7a --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Network +MCP_HOST=0.0.0.0 +MCP_PORT=3000 +MCP_ENDPOINT=/mcp +MCP_PUBLIC_URL=https://mcp.example.com + +# Authentication. A token is required unless MCP_ALLOW_NO_AUTH=true. +MCP_AUTH_TOKEN=replace-with-a-long-random-token +MCP_ALLOW_NO_AUTH=false + +# Host execution. No sandbox, approval, command allowlist, or path restriction is applied. +MCP_DEFAULT_CWD=/root +MCP_DEFAULT_SHELL=/bin/bash + +# Operational limits for transport stability, not permission restrictions. +MCP_MAX_REQUEST_BODY=8mb +MCP_MAX_OUTPUT_BYTES=1048576 +MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES=4194304 +MCP_PROCESS_RETENTION_MS=3600000 +MCP_MAX_PROCESSES=128 +MCP_SESSION_TTL_MS=86400000 +MCP_MAX_FILE_CHUNK_BYTES=1048576 +MCP_MAX_EDIT_FILE_BYTES=67108864 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc301bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Dependencies and generated output +node_modules/ +dist/ +coverage/ +.vitest/ +*.tsbuildinfo + +# Local configuration and runtime state +.env +.env.* +!.env.example +*.log +*.pid +*.pid.lock +*oauth-state*.json +.cache/ +.tmp/ +tmp/ +temp/ + +# Credentials and private keys +*.pem +*.key +*.p12 +*.pfx +*.jks +*.keystore +.npmrc +.ssh/ +id_rsa +id_ed25519 + +# Local reference material and troubleshooting captures +/codex/ +/oauth/ + +# Editors and operating systems +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp +*.swo +*~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d72d26a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 cokacremote contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dc98e0 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# cokacremote + +VPS 또는 EC2 인스턴스에서 상시 실행하는 Node.js 원격 개발 MCP 서버입니다. ChatGPT나 다른 MCP 클라이언트가 인스턴스의 셸, 프로세스, 파일 시스템을 직접 사용하도록 구성했습니다. + +이 서버에는 작업공간 샌드박스, 명령 허용목록, 실행 승인, 경로 제한이 없습니다. 도구는 MCP 서버 프로세스의 실제 OS 권한을 그대로 사용합니다. `deploy/remote-dev-mcp.service`는 요구사항에 맞춰 `root`로 실행됩니다. + +## 제공 도구 + +### 실행 및 프로세스 + +- `exec_command`: 셸 명령, 빌드, 테스트, 패키지 설치, Git, 서비스 관리, 로그 조회 +- `run_script`: Bash, sh, Node.js, Python 또는 임의 인터프리터로 전체 스크립트 실행 +- `write_stdin`: 장기 실행 프로세스에 입력을 쓰고 후속 출력 조회 +- `read_process`: 출력 커서 기반 폴링과 종료 상태 조회 +- `terminate_process`: 프로세스 그룹에 `SIGINT`, `SIGTERM`, `SIGKILL` 전달 +- `list_processes`: 실행 중이거나 최근 완료된 세션 조회 + +### 파일 시스템 + +- `list_directory`, `stat_path`, `read_file`, `write_file` +- `replace_in_file`, `apply_patch` +- `upload_file`, `download_file`, `hash_file` +- `make_directory`, `copy_path`, `move_path`, `remove_path`, `chmod_path` + +상대경로는 `MCP_DEFAULT_CWD`에서 해석되지만 절대경로와 `~/...`도 허용됩니다. 업로드와 다운로드는 `nextOffset`을 사용한 base64 청크 전송 방식입니다. + +## 로컬 실행 + +Node.js 22 이상이 필요합니다. + +```bash +npm install +npm run build + +export MCP_AUTH_TOKEN="$(openssl rand -hex 32)" +export MCP_DEFAULT_CWD=/root +npm start +``` + +기본 MCP URL은 `http://0.0.0.0:3000/mcp`, 상태 확인 URL은 `/health`입니다. + +개발 모드에서는 다음 명령을 사용할 수 있습니다. + +```bash +MCP_AUTH_TOKEN=development-token npm run dev +``` + +## 인증 방식 + +`MCP_AUTH_TOKEN`이 설정되면 모든 MCP 요청에 다음 헤더가 필요합니다. + +```http +Authorization: Bearer +``` + +ChatGPT 플러그인 연결용으로 내장 OAuth 2.1 Authorization Server를 활성화할 수 있습니다. + +```bash +MCP_OAUTH_ENABLED=true +MCP_PUBLIC_URL=https://mcp.example.com +MCP_OAUTH_STATE_FILE=/var/lib/remote-dev-mcp/oauth-state.json +``` + +활성화하면 다음 기능을 제공합니다. + +- RFC 9728 Protected Resource Metadata +- RFC 8414 Authorization Server Metadata +- Dynamic Client Registration(DCR) +- Authorization Code + PKCE(S256) +- `resource` audience 검증 +- 액세스 토큰, 회전형 refresh token, token revocation + +ChatGPT에서 연결을 승인할 때 표시되는 로그인 화면에는 `MCP_AUTH_TOKEN` 값을 입력합니다. 이 값은 승인용 비밀번호 역할도 하며, 기존처럼 정적 Bearer 토큰으로 직접 호출하는 방식도 계속 지원됩니다. 등록 클라이언트와 토큰 해시는 `MCP_OAUTH_STATE_FILE`에 권한 `600`으로 저장됩니다. + +인증을 서버 앞단의 OAuth 프록시나 사설 네트워크에서 처리한다면 다음과 같이 내장 토큰 검사를 끌 수 있습니다. + +```bash +MCP_ALLOW_NO_AUTH=true +``` + +내장 OAuth 대신 외부 IdP 또는 OAuth 게이트웨이를 사용할 수도 있습니다. 이 경우 Node 서버는 `127.0.0.1`에만 바인딩하고 앞단에서 인증을 처리합니다. ChatGPT에서 익명 MCP로 직접 연결할 경우에는 `MCP_ALLOW_NO_AUTH=true`를 사용할 수 있습니다. 이 경우 URL을 아는 누구나 인스턴스의 전체 권한을 사용할 수 있다는 점은 의도된 동작입니다. + +OpenAI의 현재 원격 MCP 인증 요구사항은 [MCP 서버 인증 문서](https://developers.openai.com/plugins/build/auth)에 정리되어 있습니다. + +## VPS/EC2 배포 + +예시는 `/opt/remote-dev-mcp`에 설치하는 경우입니다. + +```bash +sudo mkdir -p /opt/remote-dev-mcp +sudo cp -a package.json package-lock.json tsconfig.json src deploy /opt/remote-dev-mcp/ +cd /opt/remote-dev-mcp +sudo npm ci +sudo npm run build +sudo npm prune --omit=dev + +sudo cp deploy/remote-dev-mcp.env.example /etc/remote-dev-mcp.env +sudo chmod 600 /etc/remote-dev-mcp.env +sudo editor /etc/remote-dev-mcp.env + +sudo cp deploy/remote-dev-mcp.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now remote-dev-mcp +sudo systemctl status remote-dev-mcp +``` + +`/usr/bin/node`가 실제 Node.js 경로와 다르면 systemd 파일의 `ExecStart`를 수정합니다. `which node`로 확인할 수 있습니다. + +공개 인터넷에서 사용할 때는 HTTPS가 필요합니다. [Nginx 예제](deploy/nginx.remote-dev-mcp.conf)의 도메인과 인증서 경로를 바꾼 뒤 활성화합니다. Streamable HTTP의 SSE 응답을 위해 proxy buffering을 비활성화하고 긴 read timeout을 사용합니다. + +## ChatGPT 연결 + +배포 URL이 `https://mcp.example.com/mcp`라고 가정합니다. + +- ChatGPT 인증 연결: `MCP_OAUTH_ENABLED=true`로 배포하고 해당 URL을 연결한 뒤, 승인 화면에 `MCP_AUTH_TOKEN`을 입력합니다. +- ChatGPT 익명 개발 연결: 내장 인증을 끄고 해당 URL을 연결합니다. +- 외부 인증 연결: 내장 OAuth 대신 OAuth 2.1 게이트웨이나 IdP를 사용할 수 있습니다. +- OpenAI Responses API: remote MCP 도구의 서버 URL을 지정하고 서버가 요구하는 인증 토큰을 전달합니다. + +ChatGPT의 최신 연결 경로는 **Settings → Security and login → Developer mode**를 활성화한 뒤 플러그인 추가 화면에서 원격 MCP URL을 등록하는 방식입니다. 자세한 내용은 [OpenAI 원격 MCP 문서](https://developers.openai.com/api/docs/mcp)를 참고합니다. + +## 검증 + +```bash +npm run typecheck +npm test +npm run build +``` + +테스트에는 실제 Streamable HTTP MCP 클라이언트 연결, bearer 인증, 도구 목록, `run_script`, 파일 읽기·쓰기, 장기 프로세스, 청크 전송 및 unified diff 적용이 포함됩니다. + +## 주요 환경 변수 + +| 변수 | 기본값 | 설명 | +|---|---:|---| +| `MCP_HOST` | `0.0.0.0` | HTTP 바인드 주소 | +| `MCP_PORT` | `3000` | HTTP 포트 | +| `MCP_ENDPOINT` | `/mcp` | Streamable HTTP MCP 경로 | +| `MCP_PUBLIC_URL` | 없음 | 외부 HTTPS 기준 URL | +| `MCP_AUTH_TOKEN` | 없음 | bearer 토큰 | +| `MCP_ALLOW_NO_AUTH` | `false` | 인증 없이 시작 허용 | +| `MCP_OAUTH_ENABLED` | `false` | ChatGPT용 내장 OAuth 2.1/DCR 활성화 | +| `MCP_OAUTH_ISSUER` | `MCP_PUBLIC_URL` | OAuth issuer URL | +| `MCP_OAUTH_RESOURCE` | `` | MCP resource audience | +| `MCP_OAUTH_STATE_FILE` | 작업 디렉터리 내부 | 등록 클라이언트와 토큰 해시 저장 파일 | +| `MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS` | `3600` | OAuth 액세스 토큰 수명 | +| `MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS` | `2592000` | OAuth refresh token 수명 | +| `MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS` | `300` | 일회용 authorization code 수명 | +| `MCP_DEFAULT_CWD` | 서버 시작 디렉터리 | 상대경로 기준 | +| `MCP_DEFAULT_SHELL` | `$SHELL` 또는 `/bin/bash` | `exec_command` 기본 셸 | +| `MCP_MAX_OUTPUT_BYTES` | `1048576` | 한 도구 응답의 최대 출력 | +| `MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES` | `4194304` | 프로세스별 보관 출력 | +| `MCP_PROCESS_RETENTION_MS` | `3600000` | 완료 프로세스 보관 시간 | +| `MCP_SESSION_TTL_MS` | `86400000` | 유휴 MCP 세션 보관 시간 | +| `MCP_MAX_FILE_CHUNK_BYTES` | `1048576` | 파일 전송 청크 크기 | + +## 라이선스 + +[MIT License](LICENSE) diff --git a/deploy/nginx.remote-dev-mcp.conf b/deploy/nginx.remote-dev-mcp.conf new file mode 100644 index 0000000..5f83930 --- /dev/null +++ b/deploy/nginx.remote-dev-mcp.conf @@ -0,0 +1,60 @@ +server { + listen 80; + listen [::]:80; + server_name mcp.example.com; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name mcp.example.com; + + ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem; + + client_max_body_size 8m; + + location = /mcp { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 24h; + proxy_send_timeout 24h; + } + + location ^~ /.well-known/ { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache off; + } + + location ~ ^/(authorize|token|register|revoke)$ { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache off; + } + + location = /health { + proxy_pass http://127.0.0.1:3000/health; + proxy_set_header Host $host; + } +} diff --git a/deploy/remote-dev-mcp.env.example b/deploy/remote-dev-mcp.env.example new file mode 100644 index 0000000..7ce2663 --- /dev/null +++ b/deploy/remote-dev-mcp.env.example @@ -0,0 +1,24 @@ +MCP_HOST=127.0.0.1 +MCP_PORT=3000 +MCP_ENDPOINT=/mcp +MCP_PUBLIC_URL=https://mcp.example.com +MCP_ALLOWED_HOSTS=mcp.example.com,127.0.0.1,localhost +MCP_AUTH_TOKEN=replace-with-a-long-random-token +MCP_ALLOW_NO_AUTH=false +MCP_OAUTH_ENABLED=true +MCP_OAUTH_ISSUER=https://mcp.example.com +MCP_OAUTH_RESOURCE=https://mcp.example.com/mcp +MCP_OAUTH_STATE_FILE=/var/lib/remote-dev-mcp/oauth-state.json +MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS=3600 +MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS=2592000 +MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS=300 +MCP_DEFAULT_CWD=/root +MCP_DEFAULT_SHELL=/bin/bash +MCP_MAX_REQUEST_BODY=8mb +MCP_MAX_OUTPUT_BYTES=1048576 +MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES=4194304 +MCP_PROCESS_RETENTION_MS=3600000 +MCP_MAX_PROCESSES=128 +MCP_SESSION_TTL_MS=86400000 +MCP_MAX_FILE_CHUNK_BYTES=1048576 +MCP_MAX_EDIT_FILE_BYTES=67108864 diff --git a/deploy/remote-dev-mcp.service b/deploy/remote-dev-mcp.service new file mode 100644 index 0000000..3007824 --- /dev/null +++ b/deploy/remote-dev-mcp.service @@ -0,0 +1,20 @@ +[Unit] +Description=cokacremote MCP Server +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=/opt/remote-dev-mcp +EnvironmentFile=/etc/remote-dev-mcp.env +ExecStart=/usr/bin/node /opt/remote-dev-mcp/dist/src/server.js +Restart=always +RestartSec=3 +KillMode=control-group +TimeoutStopSec=20 +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..19d473f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3361 @@ +{ + "name": "cokacremote", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cokacremote", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "1.30.0", + "express": "5.2.1", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/express": "5.0.6", + "@types/node": "24.13.3", + "tsx": "4.23.12", + "typescript": "7.0.2", + "vitest": "4.1.11" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.3.tgz", + "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.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 + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@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.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.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 + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2a27abd --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "cokacremote", + "version": "0.1.0", + "private": true, + "license": "MIT", + "description": "Full-access remote development MCP server for VPS and EC2 hosts", + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "tsx watch src/server.ts", + "start": "node dist/src/server.js", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.30.0", + "express": "5.2.1", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/express": "5.0.6", + "@types/node": "24.13.3", + "tsx": "4.23.12", + "typescript": "7.0.2", + "vitest": "4.1.11" + } +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..3177a40 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,93 @@ +import { timingSafeEqual } from "node:crypto"; + +import type { RequestHandler } from "express"; +import type { OAuthTokenVerifier } from "@modelcontextprotocol/sdk/server/auth/provider.js"; + +import type { AppConfig } from "./config.js"; + +export function tokensEqual(actual: string, expected: string): boolean { + const actualBuffer = Buffer.from(actual); + const expectedBuffer = Buffer.from(expected); + return ( + actualBuffer.length === expectedBuffer.length && + timingSafeEqual(actualBuffer, expectedBuffer) + ); +} + +function oauthResourceMetadataUrl(config: AppConfig): string { + const resource = new URL(config.oauthResourceUrl!); + const suffix = resource.pathname === "/" ? "" : resource.pathname; + return new URL(`/.well-known/oauth-protected-resource${suffix}`, resource).href; +} + +export function createBearerAuth( + config: AppConfig, + oauthVerifier?: OAuthTokenVerifier, +): RequestHandler { + return async (request, response, next) => { + if (config.allowNoAuth && !config.authToken && !oauthVerifier) { + next(); + return; + } + + const authorization = request.header("authorization"); + const match = authorization?.match(/^Bearer\s+(.+)$/i); + const suppliedToken = match?.[1]; + if (suppliedToken && config.authToken && tokensEqual(suppliedToken, config.authToken)) { + next(); + return; + } + + if (suppliedToken && oauthVerifier && config.oauthResourceUrl) { + try { + const authInfo = await oauthVerifier.verifyAccessToken(suppliedToken); + const expectedResource = new URL(config.oauthResourceUrl).href; + if ( + authInfo.expiresAt !== undefined && + authInfo.expiresAt >= Date.now() / 1000 && + authInfo.resource?.href === expectedResource && + authInfo.scopes.includes("mcp:tools") + ) { + next(); + return; + } + } catch { + // Return the same challenge for every invalid token. + } + } + + const challenge = config.oauthEnabled + ? `Bearer realm="cokacremote", error="invalid_token", scope="mcp:tools", resource_metadata="${oauthResourceMetadataUrl(config)}"` + : 'Bearer realm="cokacremote"'; + response.status(401).set("WWW-Authenticate", challenge).json({ + jsonrpc: "2.0", + error: { code: -32001, message: "Unauthorized" }, + id: null, + }); + }; +} + +export function createHostValidation(config: AppConfig): RequestHandler { + return (request, response, next) => { + if (!config.allowedHosts || config.allowedHosts.length === 0) { + next(); + return; + } + const rawHost = request.header("host"); + let hostname = ""; + try { + hostname = new URL(`http://${rawHost ?? ""}`).hostname.toLowerCase(); + } catch { + // The empty value is rejected below. + } + if (!config.allowedHosts.includes(hostname)) { + response.status(403).json({ + jsonrpc: "2.0", + error: { code: -32002, message: "Host header is not allowed" }, + id: null, + }); + return; + } + next(); + }; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..ac1c6ea --- /dev/null +++ b/src/config.ts @@ -0,0 +1,200 @@ +import path from "node:path"; + +export interface AppConfig { + host: string; + port: number; + endpoint: string; + publicUrl: string | undefined; + allowedHosts: string[] | undefined; + authToken: string | undefined; + allowNoAuth: boolean; + oauthEnabled: boolean; + oauthIssuerUrl: string | undefined; + oauthResourceUrl: string | undefined; + oauthStateFile: string; + oauthAccessTokenTtlSeconds: number; + oauthRefreshTokenTtlSeconds: number; + oauthAuthorizationCodeTtlSeconds: number; + defaultCwd: string; + defaultShell: string; + maxRequestBody: string; + maxOutputBytes: number; + maxRetainedProcessOutputBytes: number; + processRetentionMs: number; + maxProcesses: number; + sessionTtlMs: number; + maxFileChunkBytes: number; + maxEditFileBytes: number; +} + +function parseBoolean(value: string | undefined, fallback: boolean): boolean { + if (value === undefined || value === "") { + return fallback; + } + if (["1", "true", "yes", "on"].includes(value.toLowerCase())) { + return true; + } + if (["0", "false", "no", "off"].includes(value.toLowerCase())) { + return false; + } + throw new Error(`Invalid boolean value: ${value}`); +} + +function parseInteger( + value: string | undefined, + fallback: number, + name: string, + minimum: number, +): number { + if (value === undefined || value === "") { + return fallback; + } + const parsed = Number.parseInt(value, 10); + if (!Number.isSafeInteger(parsed) || parsed < minimum) { + throw new Error(`${name} must be an integer greater than or equal to ${minimum}`); + } + return parsed; +} + +function normalizeEndpoint(value: string | undefined): string { + const endpoint = value?.trim() || "/mcp"; + if (!endpoint.startsWith("/")) { + throw new Error("MCP_ENDPOINT must start with '/'"); + } + return endpoint.length > 1 ? endpoint.replace(/\/+$/, "") : endpoint; +} + +function normalizeOAuthUrl(value: string | undefined, name: string): string { + if (!value) { + throw new Error(`${name} is required when MCP_OAUTH_ENABLED=true`); + } + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error(`${name} must be an absolute URL`); + } + const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1"; + if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) { + throw new Error(`${name} must use HTTPS (HTTP is allowed only for loopback tests)`); + } + if (url.search || url.hash) { + throw new Error(`${name} must not contain a query string or fragment`); + } + return url.href; +} + +export function loadConfig( + env: NodeJS.ProcessEnv = process.env, + processCwd = process.cwd(), +): AppConfig { + const allowNoAuth = parseBoolean(env.MCP_ALLOW_NO_AUTH, false); + const authToken = env.MCP_AUTH_TOKEN?.trim() || undefined; + const oauthEnabled = parseBoolean(env.MCP_OAUTH_ENABLED, false); + if (!allowNoAuth && !authToken) { + throw new Error( + "MCP_AUTH_TOKEN is required. Set MCP_ALLOW_NO_AUTH=true only when an upstream OAuth gateway or private network authenticates callers.", + ); + } + if (oauthEnabled && !authToken) { + throw new Error("MCP_AUTH_TOKEN is required as the OAuth authorization access key"); + } + + const defaultCwd = path.resolve(env.MCP_DEFAULT_CWD?.trim() || processCwd); + const allowedHosts = env.MCP_ALLOWED_HOSTS?.split(",") + .map((host) => host.trim().toLowerCase()) + .filter(Boolean); + + const endpoint = normalizeEndpoint(env.MCP_ENDPOINT); + const publicUrl = env.MCP_PUBLIC_URL?.trim().replace(/\/+$/, "") || undefined; + const oauthIssuerUrl = oauthEnabled + ? normalizeOAuthUrl(env.MCP_OAUTH_ISSUER?.trim() || publicUrl, "MCP_OAUTH_ISSUER") + : undefined; + const oauthResourceUrl = oauthEnabled + ? normalizeOAuthUrl( + env.MCP_OAUTH_RESOURCE?.trim() || (publicUrl ? `${publicUrl}${endpoint}` : undefined), + "MCP_OAUTH_RESOURCE", + ) + : undefined; + + return { + host: env.MCP_HOST?.trim() || "0.0.0.0", + port: parseInteger(env.MCP_PORT, 3000, "MCP_PORT", 1), + endpoint, + publicUrl, + allowedHosts: allowedHosts && allowedHosts.length > 0 ? allowedHosts : undefined, + authToken, + allowNoAuth, + oauthEnabled, + oauthIssuerUrl, + oauthResourceUrl, + oauthStateFile: path.resolve( + env.MCP_OAUTH_STATE_FILE?.trim() || + path.join(processCwd, ".remote-dev-mcp-oauth-state.json"), + ), + oauthAccessTokenTtlSeconds: parseInteger( + env.MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS, + 60 * 60, + "MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS", + 300, + ), + oauthRefreshTokenTtlSeconds: parseInteger( + env.MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS, + 30 * 24 * 60 * 60, + "MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS", + 3600, + ), + oauthAuthorizationCodeTtlSeconds: parseInteger( + env.MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS, + 5 * 60, + "MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS", + 60, + ), + defaultCwd, + defaultShell: + env.MCP_DEFAULT_SHELL?.trim() || env.SHELL?.trim() || "/bin/bash", + maxRequestBody: env.MCP_MAX_REQUEST_BODY?.trim() || "8mb", + maxOutputBytes: parseInteger( + env.MCP_MAX_OUTPUT_BYTES, + 1024 * 1024, + "MCP_MAX_OUTPUT_BYTES", + 16 * 1024, + ), + maxRetainedProcessOutputBytes: parseInteger( + env.MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES, + 4 * 1024 * 1024, + "MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES", + 64 * 1024, + ), + processRetentionMs: parseInteger( + env.MCP_PROCESS_RETENTION_MS, + 60 * 60 * 1000, + "MCP_PROCESS_RETENTION_MS", + 1000, + ), + maxProcesses: parseInteger( + env.MCP_MAX_PROCESSES, + 128, + "MCP_MAX_PROCESSES", + 1, + ), + sessionTtlMs: parseInteger( + env.MCP_SESSION_TTL_MS, + 24 * 60 * 60 * 1000, + "MCP_SESSION_TTL_MS", + 60_000, + ), + maxFileChunkBytes: parseInteger( + env.MCP_MAX_FILE_CHUNK_BYTES, + 1024 * 1024, + "MCP_MAX_FILE_CHUNK_BYTES", + 4096, + ), + maxEditFileBytes: parseInteger( + env.MCP_MAX_EDIT_FILE_BYTES, + 64 * 1024 * 1024, + "MCP_MAX_EDIT_FILE_BYTES", + 4096, + ), + }; +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..3e3674e --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,13 @@ +export function errorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} diff --git a/src/exec-tools.ts b/src/exec-tools.ts new file mode 100644 index 0000000..2962f72 --- /dev/null +++ b/src/exec-tools.ts @@ -0,0 +1,302 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as z from "zod/v4"; + +import type { AppConfig } from "./config.js"; +import { FileService } from "./file-service.js"; +import { ProcessManager } from "./process-manager.js"; +import { runScript } from "./script-runner.js"; +import { runTool } from "./tool-result.js"; + +const fullAccessAnnotations = { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true, +}; + +function processResult(result: Awaited>): Record { + return { + ...result, + completed: !result.running, + }; +} + +export function registerExecTools( + server: McpServer, + config: AppConfig, + processManager: ProcessManager, + fileService: FileService, +): void { + const environmentSchema = z + .record(z.string(), z.string()) + .optional() + .describe("Environment variables added to or overriding the server process environment."); + + server.registerTool( + "exec_command", + { + title: "Execute command", + description: + "Run an unrestricted shell command on the host. The command inherits the MCP server's full OS permissions, environment, filesystem, and network access. Returns output immediately when complete or a process session ID when still running.", + inputSchema: { + cmd: z.string().min(1).describe("Shell command or script to execute."), + workdir: z + .string() + .optional() + .describe(`Working directory. Relative paths resolve from ${config.defaultCwd}.`), + shell: z + .string() + .optional() + .describe(`Shell executable. Defaults to ${config.defaultShell}.`), + login: z + .boolean() + .default(true) + .describe("Use login-shell semantics (-lc) instead of -c."), + env: environmentSchema, + stdin: z.string().optional().describe("Initial text written to stdin after spawn."), + timeoutMs: z + .number() + .int() + .min(0) + .default(0) + .describe("Maximum runtime in milliseconds. Zero means no timeout."), + yieldTimeMs: z + .number() + .int() + .min(0) + .max(30_000) + .default(10_000) + .describe("How long to wait for output before returning a running session."), + maxOutputBytes: z + .number() + .int() + .min(16 * 1024) + .max(config.maxOutputBytes) + .default(config.maxOutputBytes) + .describe("Maximum output bytes returned by this call."), + }, + annotations: fullAccessAnnotations, + }, + async ({ + cmd, + workdir, + shell, + login, + env, + stdin, + timeoutMs, + yieldTimeMs, + maxOutputBytes, + }) => + runTool(async () => { + const cwd = fileService.resolve(".", workdir); + const executable = shell || config.defaultShell; + const sessionId = processManager.start({ + executable, + args: [login ? "-lc" : "-c", cmd], + commandForDisplay: cmd, + cwd, + env, + timeoutMs, + stdin, + }); + await processManager.waitForExit(sessionId, yieldTimeMs); + const result = await processManager.read(sessionId, { + maxOutputBytes, + }); + return processResult(result); + }), + ); + + server.registerTool( + "run_script", + { + title: "Run script", + description: + "Write a supplied script to a temporary executable file and run it with Bash, sh, Node.js, Python, or an arbitrary interpreter. Execution is unrestricted and has the MCP server's full host permissions.", + inputSchema: { + runtime: z + .enum(["bash", "sh", "node", "python", "custom"]) + .default("bash") + .describe("Script runtime. Use custom with interpreter for any other runtime."), + script: z.string().describe("Complete script source."), + workdir: z + .string() + .optional() + .describe(`Working directory. Relative paths resolve from ${config.defaultCwd}.`), + args: z.array(z.string()).default([]).describe("Arguments passed after the script path."), + env: environmentSchema, + interpreter: z + .string() + .optional() + .describe("Interpreter executable override. Required for runtime=custom."), + interpreterArgs: z + .array(z.string()) + .default([]) + .describe("Arguments placed before the temporary script path."), + stdin: z.string().optional().describe("Initial text written to the script stdin."), + timeoutMs: z + .number() + .int() + .min(0) + .default(0) + .describe("Maximum runtime in milliseconds. Zero means no timeout."), + yieldTimeMs: z + .number() + .int() + .min(0) + .max(30_000) + .default(10_000), + maxOutputBytes: z + .number() + .int() + .min(16 * 1024) + .max(config.maxOutputBytes) + .default(config.maxOutputBytes), + keepScript: z + .boolean() + .default(false) + .describe("Keep the temporary script after the process exits and return its path."), + }, + annotations: fullAccessAnnotations, + }, + async ({ + runtime, + script, + workdir, + args, + env, + interpreter, + interpreterArgs, + stdin, + timeoutMs, + yieldTimeMs, + maxOutputBytes, + keepScript, + }) => + runTool(async () => { + const result = await runScript(processManager, { + runtime, + script, + cwd: fileService.resolve(".", workdir), + args, + env, + interpreter, + interpreterArgs, + stdin, + timeoutMs, + yieldTimeMs, + maxOutputBytes, + keepScript, + }); + return processResult(result); + }), + ); + + server.registerTool( + "write_stdin", + { + title: "Write to process stdin", + description: + "Write text to an existing process session, optionally close stdin, then return new output.", + inputSchema: { + sessionId: z.string().uuid(), + chars: z.string().default(""), + closeStdin: z.boolean().default(false), + afterSeq: z.number().int().min(0).default(0), + yieldTimeMs: z.number().int().min(0).max(300_000).default(250), + maxOutputBytes: z + .number() + .int() + .min(16 * 1024) + .max(config.maxOutputBytes) + .default(config.maxOutputBytes), + }, + annotations: fullAccessAnnotations, + }, + async ({ sessionId, chars, closeStdin, afterSeq, yieldTimeMs, maxOutputBytes }) => + runTool(async () => { + await processManager.write(sessionId, chars, closeStdin); + if (closeStdin) { + await processManager.waitForExit(sessionId, yieldTimeMs); + } + const result = await processManager.read(sessionId, { + afterSeq, + waitMs: closeStdin ? 0 : yieldTimeMs, + maxOutputBytes, + }); + return processResult(result); + }), + ); + + server.registerTool( + "read_process", + { + title: "Read process output", + description: + "Poll a managed process for output and terminal state. Pass the previous nextSeq as afterSeq to receive only newer output.", + inputSchema: { + sessionId: z.string().uuid(), + afterSeq: z.number().int().min(0).default(0), + waitMs: z.number().int().min(0).max(300_000).default(1000), + maxOutputBytes: z + .number() + .int() + .min(16 * 1024) + .max(config.maxOutputBytes) + .default(config.maxOutputBytes), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async ({ sessionId, afterSeq, waitMs, maxOutputBytes }) => + runTool(async () => + processResult( + await processManager.read(sessionId, { + afterSeq, + waitMs, + maxOutputBytes, + }), + ), + ), + ); + + server.registerTool( + "terminate_process", + { + title: "Terminate process", + description: + "Send a signal to a managed process tree. SIGTERM escalates to SIGKILL after graceMs if necessary.", + inputSchema: { + sessionId: z.string().uuid(), + signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]).default("SIGTERM"), + graceMs: z.number().int().min(0).max(60_000).default(3000), + }, + annotations: fullAccessAnnotations, + }, + async ({ sessionId, signal, graceMs }) => + runTool(async () => + processResult(await processManager.terminate(sessionId, signal, graceMs)), + ), + ); + + server.registerTool( + "list_processes", + { + title: "List managed processes", + description: "List running and recently completed process sessions.", + inputSchema: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async () => runTool(() => ({ processes: processManager.list() })), + ); +} diff --git a/src/file-service.ts b/src/file-service.ts new file mode 100644 index 0000000..693a72e --- /dev/null +++ b/src/file-service.ts @@ -0,0 +1,526 @@ +import { createHash, randomUUID } from "node:crypto"; +import { createReadStream } from "node:fs"; +import { + appendFile, + chmod, + copyFile, + cp, + lstat, + mkdir, + mkdtemp, + open, + readFile, + readdir, + readlink, + rename, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { execFile } from "node:child_process"; + +import { errorMessage } from "./errors.js"; +import { expandPath } from "./paths.js"; + +const execFileAsync = promisify(execFile); + +export type FileContentEncoding = "utf8" | "base64"; + +export interface FileServiceOptions { + defaultCwd: string; + maxChunkBytes: number; + maxEditFileBytes: number; + maxOutputBytes: number; +} + +export interface ListDirectoryOptions { + recursive?: boolean; + maxDepth?: number; + maxEntries?: number; + includeHidden?: boolean; + includeMetadata?: boolean; +} + +interface DirectoryEntryResult { + path: string; + relativePath: string; + name: string; + type: "file" | "directory" | "symlink" | "other"; + size?: number; + mode?: string; + modifiedAt?: string; +} + +function typeFromStats(stats: Awaited>): DirectoryEntryResult["type"] { + if (stats.isFile()) { + return "file"; + } + if (stats.isDirectory()) { + return "directory"; + } + if (stats.isSymbolicLink()) { + return "symlink"; + } + return "other"; +} + +function encodeContent(data: Buffer, encoding: FileContentEncoding): string { + return encoding === "base64" ? data.toString("base64") : data.toString("utf8"); +} + +function decodeContent(data: string, encoding: FileContentEncoding): Buffer { + return Buffer.from(data, encoding === "base64" ? "base64" : "utf8"); +} + +export class FileService { + readonly #options: FileServiceOptions; + + constructor(options: FileServiceOptions) { + this.#options = options; + } + + resolve(inputPath: string, cwd?: string): string { + const base = cwd + ? expandPath(cwd, this.#options.defaultCwd) + : this.#options.defaultCwd; + return expandPath(inputPath, base); + } + + async getInfo(inputPath: string, cwd?: string): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + const info = await lstat(resolvedPath); + const result: Record = { + path: resolvedPath, + type: typeFromStats(info), + size: info.size, + mode: `0${(info.mode & 0o7777).toString(8)}`, + uid: info.uid, + gid: info.gid, + createdAt: info.birthtime.toISOString(), + modifiedAt: info.mtime.toISOString(), + accessedAt: info.atime.toISOString(), + }; + if (info.isSymbolicLink()) { + result.symlinkTarget = await readlink(resolvedPath); + } + return result; + } + + async listDirectory( + inputPath: string, + cwd: string | undefined, + options: ListDirectoryOptions = {}, + ): Promise> { + const root = this.resolve(inputPath, cwd); + const recursive = options.recursive ?? false; + const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 8, 100)); + const maxEntries = Math.max(1, Math.min(options.maxEntries ?? 1000, 50_000)); + const includeHidden = options.includeHidden ?? true; + const includeMetadata = options.includeMetadata ?? false; + const entries: DirectoryEntryResult[] = []; + let truncated = false; + + const visit = async (directory: string, depth: number): Promise => { + if (truncated) { + return; + } + const directoryEntries = await readdir(directory, { withFileTypes: true }); + directoryEntries.sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of directoryEntries) { + if (!includeHidden && entry.name.startsWith(".")) { + continue; + } + if (entries.length >= maxEntries) { + truncated = true; + return; + } + const absolutePath = path.join(directory, entry.name); + const relativePath = path.relative(root, absolutePath) || entry.name; + const info = await lstat(absolutePath); + const result: DirectoryEntryResult = { + path: absolutePath, + relativePath, + name: entry.name, + type: typeFromStats(info), + }; + if (includeMetadata) { + result.size = info.size; + result.mode = `0${(info.mode & 0o7777).toString(8)}`; + result.modifiedAt = info.mtime.toISOString(); + } + entries.push(result); + if (recursive && info.isDirectory() && depth < maxDepth) { + await visit(absolutePath, depth + 1); + } + } + }; + + await visit(root, 0); + return { + path: root, + entries, + count: entries.length, + truncated, + }; + } + + async readFileChunk( + inputPath: string, + cwd: string | undefined, + offset = 0, + maxBytes = 256 * 1024, + encoding: FileContentEncoding = "utf8", + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + const info = await stat(resolvedPath); + if (!info.isFile()) { + throw new Error(`${resolvedPath} is not a regular file`); + } + const safeOffset = Math.max(0, Math.min(offset, info.size)); + const byteCount = Math.max( + 1, + Math.min(maxBytes, this.#options.maxChunkBytes, info.size - safeOffset), + ); + const handle = await open(resolvedPath, "r"); + try { + const buffer = Buffer.alloc(byteCount); + const { bytesRead } = await handle.read(buffer, 0, byteCount, safeOffset); + const data = buffer.subarray(0, bytesRead); + const nextOffset = safeOffset + bytesRead; + return { + path: resolvedPath, + encoding, + content: encodeContent(data, encoding), + offset: safeOffset, + nextOffset, + bytesRead, + totalBytes: info.size, + eof: nextOffset >= info.size, + }; + } finally { + await handle.close(); + } + } + + async writeFileContent( + inputPath: string, + cwd: string | undefined, + content: string, + encoding: FileContentEncoding, + mode: "overwrite" | "append", + createParents: boolean, + fileMode?: number, + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + if (createParents) { + await mkdir(path.dirname(resolvedPath), { recursive: true }); + } + const data = decodeContent(content, encoding); + if (mode === "append") { + await appendFile(resolvedPath, data, fileMode === undefined ? undefined : { mode: fileMode }); + } else { + await writeFile(resolvedPath, data, fileMode === undefined ? undefined : { mode: fileMode }); + } + const info = await stat(resolvedPath); + return { + path: resolvedPath, + bytesWritten: data.length, + totalBytes: info.size, + mode, + }; + } + + async uploadChunk( + inputPath: string, + cwd: string | undefined, + dataBase64: string, + offset: number, + truncate: boolean, + createParents: boolean, + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + const data = Buffer.from(dataBase64, "base64"); + if (data.length > this.#options.maxChunkBytes) { + throw new Error( + `Upload chunk is ${data.length} bytes; maximum is ${this.#options.maxChunkBytes}`, + ); + } + if (createParents) { + await mkdir(path.dirname(resolvedPath), { recursive: true }); + } + + let handle; + try { + handle = await open(resolvedPath, "r+"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + handle = await open(resolvedPath, "w+"); + } + try { + if (truncate) { + await handle.truncate(0); + } + const safeOffset = Math.max(0, offset); + const { bytesWritten } = await handle.write(data, 0, data.length, safeOffset); + const info = await handle.stat(); + return { + path: resolvedPath, + offset: safeOffset, + bytesWritten, + nextOffset: safeOffset + bytesWritten, + totalBytes: info.size, + chunkSha256: createHash("sha256").update(data).digest("hex"), + }; + } finally { + await handle.close(); + } + } + + async downloadChunk( + inputPath: string, + cwd: string | undefined, + offset: number, + maxBytes: number, + ): Promise> { + const result = await this.readFileChunk( + inputPath, + cwd, + offset, + maxBytes, + "base64", + ); + return { + path: result.path, + dataBase64: result.content, + offset: result.offset, + nextOffset: result.nextOffset, + bytesRead: result.bytesRead, + totalBytes: result.totalBytes, + eof: result.eof, + }; + } + + async replaceInFile( + inputPath: string, + cwd: string | undefined, + oldText: string, + newText: string, + replaceAll: boolean, + expectedOccurrences: number | undefined, + ): Promise> { + if (oldText.length === 0) { + throw new Error("oldText must not be empty"); + } + const resolvedPath = this.resolve(inputPath, cwd); + const info = await stat(resolvedPath); + if (info.size > this.#options.maxEditFileBytes) { + throw new Error( + `${resolvedPath} is ${info.size} bytes; replace_in_file limit is ${this.#options.maxEditFileBytes}`, + ); + } + const original = await readFile(resolvedPath, "utf8"); + const occurrences = original.split(oldText).length - 1; + const expected = expectedOccurrences ?? (replaceAll ? occurrences : 1); + if (occurrences !== expected) { + throw new Error( + `Expected ${expected} occurrence(s) of oldText in ${resolvedPath}, found ${occurrences}`, + ); + } + const updated = replaceAll + ? original.split(oldText).join(newText) + : original.replace(oldText, newText); + await writeFile(resolvedPath, updated, "utf8"); + return { + path: resolvedPath, + replacements: replaceAll ? occurrences : Math.min(occurrences, 1), + previousBytes: Buffer.byteLength(original), + currentBytes: Buffer.byteLength(updated), + }; + } + + async applyPatch( + patchText: string, + cwd: string | undefined, + options: { checkOnly: boolean; reverse: boolean; threeWay: boolean }, + ): Promise> { + const resolvedCwd = this.resolve(".", cwd); + const temporaryDirectory = await mkdtemp( + path.join(os.tmpdir(), "remote-dev-mcp-patch-"), + ); + const patchPath = path.join(temporaryDirectory, `${randomUUID()}.patch`); + await writeFile(patchPath, patchText, "utf8"); + + const baseArguments = ["apply", "--unsafe-paths", "--whitespace=nowarn"]; + if (options.reverse) { + baseArguments.push("--reverse"); + } + if (options.threeWay) { + baseArguments.push("--3way"); + } + const checkArguments = [...baseArguments, "--check", patchPath]; + try { + const checked = await execFileAsync("git", checkArguments, { + cwd: resolvedCwd, + encoding: "utf8", + maxBuffer: this.#options.maxOutputBytes, + }); + if (options.checkOnly) { + return { + cwd: resolvedCwd, + applied: false, + checkOnly: true, + stdout: checked.stdout, + stderr: checked.stderr, + }; + } + const applied = await execFileAsync("git", [...baseArguments, patchPath], { + cwd: resolvedCwd, + encoding: "utf8", + maxBuffer: this.#options.maxOutputBytes, + }); + return { + cwd: resolvedCwd, + applied: true, + checkOnly: false, + stdout: applied.stdout, + stderr: applied.stderr, + }; + } catch (error) { + const execError = error as Error & { stdout?: string; stderr?: string }; + throw new Error( + [errorMessage(execError), execError.stdout, execError.stderr] + .filter(Boolean) + .join("\n"), + ); + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } + } + + async makeDirectory( + inputPath: string, + cwd: string | undefined, + recursive: boolean, + mode?: number, + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + await mkdir(resolvedPath, { + recursive, + ...(mode === undefined ? {} : { mode }), + }); + return { path: resolvedPath, created: true }; + } + + async copyPath( + sourcePath: string, + destinationPath: string, + cwd: string | undefined, + recursive: boolean, + force: boolean, + ): Promise> { + const source = this.resolve(sourcePath, cwd); + const destination = this.resolve(destinationPath, cwd); + if (source === destination) { + throw new Error("Source and destination paths must be different"); + } + const sourceInfo = await lstat(source); + if (sourceInfo.isDirectory()) { + if (!recursive) { + throw new Error("recursive=true is required to copy a directory"); + } + await cp(source, destination, { recursive: true, force }); + } else { + if (!force) { + try { + await lstat(destination); + throw new Error(`Destination already exists: ${destination}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + } + await mkdir(path.dirname(destination), { recursive: true }); + await copyFile(source, destination); + } + return { source, destination, copied: true }; + } + + async movePath( + sourcePath: string, + destinationPath: string, + cwd: string | undefined, + overwrite: boolean, + ): Promise> { + const source = this.resolve(sourcePath, cwd); + const destination = this.resolve(destinationPath, cwd); + if (source === destination) { + return { source, destination, moved: false, samePath: true }; + } + if (!overwrite) { + try { + await lstat(destination); + throw new Error(`Destination already exists: ${destination}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + } else { + await rm(destination, { recursive: true, force: true }); + } + await mkdir(path.dirname(destination), { recursive: true }); + try { + await rename(source, destination); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EXDEV") { + throw error; + } + await cp(source, destination, { recursive: true, force: overwrite }); + await rm(source, { recursive: true, force: true }); + } + return { source, destination, moved: true }; + } + + async removePath( + inputPath: string, + cwd: string | undefined, + recursive: boolean, + force: boolean, + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + await rm(resolvedPath, { recursive, force }); + return { path: resolvedPath, removed: true }; + } + + async changeMode( + inputPath: string, + cwd: string | undefined, + mode: number, + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + await chmod(resolvedPath, mode); + return { path: resolvedPath, mode: `0${mode.toString(8)}` }; + } + + async hashFile( + inputPath: string, + cwd: string | undefined, + algorithm: "sha256" | "sha512" | "md5", + ): Promise> { + const resolvedPath = this.resolve(inputPath, cwd); + const hash = createHash(algorithm); + await new Promise((resolve, reject) => { + const stream = createReadStream(resolvedPath); + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("error", reject); + stream.on("end", resolve); + }); + return { path: resolvedPath, algorithm, digest: hash.digest("hex") }; + } +} diff --git a/src/file-tools.ts b/src/file-tools.ts new file mode 100644 index 0000000..c9b9d7a --- /dev/null +++ b/src/file-tools.ts @@ -0,0 +1,339 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as z from "zod/v4"; + +import type { AppConfig } from "./config.js"; +import { FileService } from "./file-service.js"; +import { runTool } from "./tool-result.js"; + +const readAnnotations = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; + +const writeAnnotations = { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: false, +}; + +const cwdSchema = z + .string() + .optional() + .describe("Base directory used to resolve relative paths."); + +const pathSchema = z + .string() + .min(1) + .describe("Absolute path, ~/ path, or a path relative to cwd/default cwd."); + +const fileModeSchema = z + .string() + .regex(/^(?:0o)?[0-7]{3,4}$/) + .optional() + .describe("Unix mode written as an octal string, for example 0755."); + +function parseMode(mode: string | undefined): number | undefined { + if (mode === undefined) { + return undefined; + } + return Number.parseInt(mode.replace(/^0o/, ""), 8); +} + +export function registerFileTools( + server: McpServer, + config: AppConfig, + files: FileService, +): void { + server.registerTool( + "list_directory", + { + title: "List directory", + description: + "List any host directory. Recursive listing does not follow directory symlinks.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + recursive: z.boolean().default(false), + maxDepth: z.number().int().min(0).max(100).default(8), + maxEntries: z.number().int().min(1).max(50_000).default(1000), + includeHidden: z.boolean().default(true), + includeMetadata: z.boolean().default(false), + }, + annotations: readAnnotations, + }, + async ({ path, cwd, recursive, maxDepth, maxEntries, includeHidden, includeMetadata }) => + runTool(() => + files.listDirectory(path, cwd, { + recursive, + maxDepth, + maxEntries, + includeHidden, + includeMetadata, + }), + ), + ); + + server.registerTool( + "stat_path", + { + title: "Inspect path", + description: "Return metadata for any file, directory, or symbolic link.", + inputSchema: { path: pathSchema, cwd: cwdSchema }, + annotations: readAnnotations, + }, + async ({ path, cwd }) => runTool(() => files.getInfo(path, cwd)), + ); + + server.registerTool( + "read_file", + { + title: "Read file", + description: + "Read a bounded chunk of any host file as UTF-8 text or base64. Continue with nextOffset until eof=true.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + offset: z.number().int().min(0).default(0), + maxBytes: z + .number() + .int() + .min(1) + .max(config.maxFileChunkBytes) + .default(Math.min(256 * 1024, config.maxFileChunkBytes)), + encoding: z.enum(["utf8", "base64"]).default("utf8"), + }, + annotations: readAnnotations, + }, + async ({ path, cwd, offset, maxBytes, encoding }) => + runTool(() => files.readFileChunk(path, cwd, offset, maxBytes, encoding)), + ); + + server.registerTool( + "write_file", + { + title: "Write file", + description: + "Create, overwrite, or append to any host file using UTF-8 or base64 content.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + content: z.string(), + encoding: z.enum(["utf8", "base64"]).default("utf8"), + mode: z.enum(["overwrite", "append"]).default("overwrite"), + createParents: z.boolean().default(true), + fileMode: fileModeSchema, + }, + annotations: writeAnnotations, + }, + async ({ path, cwd, content, encoding, mode, createParents, fileMode }) => + runTool(() => + files.writeFileContent( + path, + cwd, + content, + encoding, + mode, + createParents, + parseMode(fileMode), + ), + ), + ); + + server.registerTool( + "replace_in_file", + { + title: "Replace text in file", + description: + "Perform an exact text replacement in a UTF-8 file. By default exactly one occurrence must exist, preventing ambiguous edits.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + oldText: z.string().min(1), + newText: z.string(), + replaceAll: z.boolean().default(false), + expectedOccurrences: z.number().int().min(0).optional(), + }, + annotations: writeAnnotations, + }, + async ({ path, cwd, oldText, newText, replaceAll, expectedOccurrences }) => + runTool(() => + files.replaceInFile( + path, + cwd, + oldText, + newText, + replaceAll, + expectedOccurrences, + ), + ), + ); + + server.registerTool( + "apply_patch", + { + title: "Apply unified diff", + description: + "Validate and apply a standard unified diff with git apply. Paths are unrestricted and --unsafe-paths is enabled.", + inputSchema: { + patch: z.string().min(1).describe("Standard unified diff text."), + cwd: cwdSchema, + checkOnly: z.boolean().default(false), + reverse: z.boolean().default(false), + threeWay: z.boolean().default(false), + }, + annotations: writeAnnotations, + }, + async ({ patch, cwd, checkOnly, reverse, threeWay }) => + runTool(() => files.applyPatch(patch, cwd, { checkOnly, reverse, threeWay })), + ); + + server.registerTool( + "upload_file", + { + title: "Upload file chunk", + description: + "Write a base64 file chunk at an exact byte offset. Use truncate=true for the first chunk of a replacement upload, then continue with nextOffset.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + dataBase64: z.string(), + offset: z.number().int().min(0).default(0), + truncate: z.boolean().default(false), + createParents: z.boolean().default(true), + }, + annotations: writeAnnotations, + }, + async ({ path, cwd, dataBase64, offset, truncate, createParents }) => + runTool(() => + files.uploadChunk(path, cwd, dataBase64, offset, truncate, createParents), + ), + ); + + server.registerTool( + "download_file", + { + title: "Download file chunk", + description: + "Read a file chunk as base64. Continue with nextOffset until eof=true.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + offset: z.number().int().min(0).default(0), + maxBytes: z + .number() + .int() + .min(1) + .max(config.maxFileChunkBytes) + .default(config.maxFileChunkBytes), + }, + annotations: readAnnotations, + }, + async ({ path, cwd, offset, maxBytes }) => + runTool(() => files.downloadChunk(path, cwd, offset, maxBytes)), + ); + + server.registerTool( + "make_directory", + { + title: "Create directory", + description: "Create any host directory.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + recursive: z.boolean().default(true), + mode: fileModeSchema, + }, + annotations: writeAnnotations, + }, + async ({ path, cwd, recursive, mode }) => + runTool(() => files.makeDirectory(path, cwd, recursive, parseMode(mode))), + ); + + server.registerTool( + "copy_path", + { + title: "Copy path", + description: "Copy a file or directory anywhere on the host.", + inputSchema: { + sourcePath: pathSchema, + destinationPath: pathSchema, + cwd: cwdSchema, + recursive: z.boolean().default(true), + force: z.boolean().default(true), + }, + annotations: writeAnnotations, + }, + async ({ sourcePath, destinationPath, cwd, recursive, force }) => + runTool(() => files.copyPath(sourcePath, destinationPath, cwd, recursive, force)), + ); + + server.registerTool( + "move_path", + { + title: "Move path", + description: "Move or rename a file or directory anywhere on the host.", + inputSchema: { + sourcePath: pathSchema, + destinationPath: pathSchema, + cwd: cwdSchema, + overwrite: z.boolean().default(false), + }, + annotations: writeAnnotations, + }, + async ({ sourcePath, destinationPath, cwd, overwrite }) => + runTool(() => files.movePath(sourcePath, destinationPath, cwd, overwrite)), + ); + + server.registerTool( + "remove_path", + { + title: "Remove path", + description: + "Permanently remove any host file or directory. This operation is not restricted to a workspace and does not use trash.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + recursive: z.boolean().default(false), + force: z.boolean().default(false), + }, + annotations: writeAnnotations, + }, + async ({ path, cwd, recursive, force }) => + runTool(() => files.removePath(path, cwd, recursive, force)), + ); + + server.registerTool( + "chmod_path", + { + title: "Change path mode", + description: "Change Unix permission bits on any host path.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + mode: z.string().regex(/^(?:0o)?[0-7]{3,4}$/), + }, + annotations: writeAnnotations, + }, + async ({ path, cwd, mode }) => + runTool(() => files.changeMode(path, cwd, parseMode(mode) ?? 0)), + ); + + server.registerTool( + "hash_file", + { + title: "Hash file", + description: "Calculate a digest for any host file, useful for transfer verification.", + inputSchema: { + path: pathSchema, + cwd: cwdSchema, + algorithm: z.enum(["sha256", "sha512", "md5"]).default("sha256"), + }, + annotations: readAnnotations, + }, + async ({ path, cwd, algorithm }) => + runTool(() => files.hashFile(path, cwd, algorithm)), + ); +} diff --git a/src/http-server.ts b/src/http-server.ts new file mode 100644 index 0000000..01f8675 --- /dev/null +++ b/src/http-server.ts @@ -0,0 +1,211 @@ +import { randomUUID } from "node:crypto"; +import type { Server as HttpServer } from "node:http"; + +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import express, { type Request, type Response } from "express"; + +import { createBearerAuth, createHostValidation } from "./auth.js"; +import type { AppConfig } from "./config.js"; +import { errorMessage } from "./errors.js"; +import { createMcpServer, type McpServices } from "./mcp-server.js"; +import { OAUTH_SCOPES, RemoteDevOAuthProvider } from "./oauth.js"; + +interface ActiveSession { + transport: StreamableHTTPServerTransport; + server: ReturnType; + lastUsedAt: number; +} + +export interface RunningHttpServer { + httpServer: HttpServer; + close: () => Promise; +} + +function rpcError(response: Response, status: number, message: string): void { + response.status(status).json({ + jsonrpc: "2.0", + error: { code: -32000, message }, + id: null, + }); +} + +export async function startHttpServer( + config: AppConfig, + services: McpServices, +): Promise { + const app = express(); + app.disable("x-powered-by"); + app.set("trust proxy", 1); + app.use(express.json({ limit: config.maxRequestBody })); + app.use(createHostValidation(config)); + + const sessions = new Map(); + const oauthProvider = config.oauthEnabled ? new RemoteDevOAuthProvider(config) : undefined; + if (oauthProvider) { + app.get("/.well-known/oauth-protected-resource", (_request, response) => { + response.json({ + resource: oauthProvider.resourceUrl.href, + authorization_servers: [oauthProvider.issuerUrl.href], + scopes_supported: [...OAUTH_SCOPES], + bearer_methods_supported: ["header"], + resource_name: "cokacremote", + }); + }); + app.use( + mcpAuthRouter({ + provider: oauthProvider, + issuerUrl: oauthProvider.issuerUrl, + resourceServerUrl: oauthProvider.resourceUrl, + scopesSupported: [...OAUTH_SCOPES], + resourceName: "cokacremote", + clientRegistrationOptions: { clientSecretExpirySeconds: 0 }, + }), + ); + } + const authenticate = createBearerAuth(config, oauthProvider); + + app.get("/health", (_request, response) => { + response.json({ + status: "ok", + service: "cokacremote", + version: "0.1.0", + activeMcpSessions: sessions.size, + managedProcesses: services.processManager.list().length, + unrestrictedHostAccess: true, + oauthEnabled: config.oauthEnabled, + }); + }); + + const postHandler = async (request: Request, response: Response): Promise => { + const sessionId = request.header("mcp-session-id"); + try { + if (sessionId) { + const session = sessions.get(sessionId); + if (!session) { + rpcError(response, 404, "Unknown or expired MCP session"); + return; + } + session.lastUsedAt = Date.now(); + await session.transport.handleRequest(request, response, request.body); + return; + } + + if (!isInitializeRequest(request.body)) { + rpcError(response, 400, "An initialize request or valid MCP session ID is required"); + return; + } + + let activeSession: ActiveSession; + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (initializedSessionId) => { + activeSession.lastUsedAt = Date.now(); + sessions.set(initializedSessionId, activeSession); + }, + }); + const server = createMcpServer(config, services); + activeSession = { transport, server, lastUsedAt: Date.now() }; + transport.onclose = () => { + const closedSessionId = transport.sessionId; + if (closedSessionId) { + sessions.delete(closedSessionId); + } + }; + transport.onerror = (error) => { + console.error("MCP transport error:", errorMessage(error)); + }; + await server.connect(transport); + await transport.handleRequest(request, response, request.body); + } catch (error) { + console.error("MCP POST failed:", errorMessage(error)); + if (!response.headersSent) { + rpcError(response, 500, "Internal MCP server error"); + } + } + }; + + const sessionHandler = async (request: Request, response: Response): Promise => { + const sessionId = request.header("mcp-session-id"); + if (!sessionId) { + rpcError(response, 400, "MCP-Session-Id header is required"); + return; + } + const session = sessions.get(sessionId); + if (!session) { + rpcError(response, 404, "Unknown or expired MCP session"); + return; + } + session.lastUsedAt = Date.now(); + try { + await session.transport.handleRequest(request, response); + } catch (error) { + console.error(`MCP ${request.method} failed:`, errorMessage(error)); + if (!response.headersSent) { + rpcError(response, 500, "Internal MCP server error"); + } + } + }; + + app.post(config.endpoint, authenticate, (request, response) => { + void postHandler(request, response); + }); + app.get(config.endpoint, authenticate, (request, response) => { + void sessionHandler(request, response); + }); + app.delete(config.endpoint, authenticate, (request, response) => { + void sessionHandler(request, response); + }); + + app.use( + ( + error: unknown, + _request: Request, + response: Response, + _next: express.NextFunction, + ) => { + if (!response.headersSent) { + rpcError(response, 400, `Invalid request body: ${errorMessage(error)}`); + } + }, + ); + + const cleanupInterval = setInterval(() => { + const cutoff = Date.now() - config.sessionTtlMs; + for (const [sessionId, session] of sessions) { + if (session.lastUsedAt < cutoff) { + sessions.delete(sessionId); + void session.server.close().catch((error) => { + console.error(`Failed to close expired session ${sessionId}:`, errorMessage(error)); + }); + } + } + services.processManager.prune(); + }, Math.min(config.sessionTtlMs, 60_000)); + cleanupInterval.unref(); + + const httpServer = await new Promise((resolve, reject) => { + const listeningServer = app.listen(config.port, config.host, () => resolve(listeningServer)); + listeningServer.once("error", reject); + }); + + const close = async (): Promise => { + clearInterval(cleanupInterval); + const activeSessions = [...sessions.values()]; + sessions.clear(); + await Promise.allSettled(activeSessions.map((session) => session.server.close())); + await services.processManager.shutdown(); + await new Promise((resolve, reject) => { + httpServer.close((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + }; + + return { httpServer, close }; +} diff --git a/src/mcp-server.ts b/src/mcp-server.ts new file mode 100644 index 0000000..6cadd40 --- /dev/null +++ b/src/mcp-server.ts @@ -0,0 +1,53 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +import type { AppConfig } from "./config.js"; +import { registerExecTools } from "./exec-tools.js"; +import { FileService } from "./file-service.js"; +import { registerFileTools } from "./file-tools.js"; +import { ProcessManager } from "./process-manager.js"; + +export interface McpServices { + processManager: ProcessManager; + fileService: FileService; +} + +export function createServices(config: AppConfig): McpServices { + return { + processManager: new ProcessManager({ + maxRetainedOutputBytes: config.maxRetainedProcessOutputBytes, + processRetentionMs: config.processRetentionMs, + maxProcesses: config.maxProcesses, + defaultMaxOutputBytes: config.maxOutputBytes, + }), + fileService: new FileService({ + defaultCwd: config.defaultCwd, + maxChunkBytes: config.maxFileChunkBytes, + maxEditFileBytes: config.maxEditFileBytes, + maxOutputBytes: config.maxOutputBytes, + }), + }; +} + +export function createMcpServer(config: AppConfig, services: McpServices): McpServer { + const server = new McpServer( + { + name: "cokacremote", + version: "0.1.0", + ...(config.publicUrl ? { websiteUrl: config.publicUrl } : {}), + }, + { + instructions: + "This server is an unrestricted remote development environment. Tools operate directly on the host with the MCP service process's full OS permissions. Use exec_command for shell, build, test, package, Git, service, and log workflows; run_script for complete Bash, Node.js, or Python scripts; and the file tools for direct file operations. Poll long-running commands with read_process or write_stdin.", + capabilities: { logging: {} }, + }, + ); + + registerExecTools( + server, + config, + services.processManager, + services.fileService, + ); + registerFileTools(server, config, services.fileService); + return server; +} diff --git a/src/oauth.ts b/src/oauth.ts new file mode 100644 index 0000000..7797dc2 --- /dev/null +++ b/src/oauth.ts @@ -0,0 +1,513 @@ +import { createHash, randomBytes, randomUUID } from "node:crypto"; +import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; +import path from "node:path"; + +import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js"; +import { + InvalidGrantError, + InvalidScopeError, + InvalidTargetError, +} from "@modelcontextprotocol/sdk/server/auth/errors.js"; +import type { + AuthorizationParams, + OAuthServerProvider, +} from "@modelcontextprotocol/sdk/server/auth/provider.js"; +import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; +import type { + OAuthClientInformationFull, + OAuthTokenRevocationRequest, + OAuthTokens, +} from "@modelcontextprotocol/sdk/shared/auth.js"; +import type { Request, Response } from "express"; + +import { tokensEqual } from "./auth.js"; +import type { AppConfig } from "./config.js"; + +export const OAUTH_SCOPES = ["mcp:tools"] as const; + +interface StoredToken { + type: "access" | "refresh"; + clientId: string; + scopes: string[]; + expiresAt: number; + resource: string; +} + +interface PersistedOAuthState { + version: 1; + clients: Record; + tokens: Record; +} + +interface AuthorizationCodeRecord { + clientId: string; + codeChallenge: string; + redirectUri: string; + resource: string; + scopes: string[]; + expiresAt: number; +} + +type RefreshResult = + | { status: "invalid" } + | { status: "invalid_scope" } + | { status: "ok"; tokens: OAuthTokens }; + +function emptyState(): PersistedOAuthState { + return { version: 1, clients: {}, tokens: {} }; +} + +function tokenHash(token: string): string { + return createHash("sha256").update(token).digest("hex"); +} + +function randomToken(): string { + return randomBytes(32).toString("base64url"); +} + +function isStoredToken(value: unknown): value is StoredToken { + if (!value || typeof value !== "object") { + return false; + } + const token = value as Partial; + return ( + (token.type === "access" || token.type === "refresh") && + typeof token.clientId === "string" && + Array.isArray(token.scopes) && + token.scopes.every((scope) => typeof scope === "string") && + typeof token.expiresAt === "number" && + typeof token.resource === "string" + ); +} + +function parseState(value: string): PersistedOAuthState { + const parsed = JSON.parse(value) as Partial; + if ( + parsed.version !== 1 || + !parsed.clients || + typeof parsed.clients !== "object" || + !parsed.tokens || + typeof parsed.tokens !== "object" || + !Object.values(parsed.tokens).every(isStoredToken) + ) { + throw new Error("Invalid OAuth state file format"); + } + return parsed as PersistedOAuthState; +} + +class PersistentOAuthStore implements OAuthRegisteredClientsStore { + private state = emptyState(); + private loadPromise: Promise | undefined; + private mutationQueue: Promise = Promise.resolve(); + + constructor( + private readonly stateFile: string, + private readonly accessTokenTtlSeconds: number, + private readonly refreshTokenTtlSeconds: number, + ) {} + + private async ensureLoaded(): Promise { + this.loadPromise ??= (async () => { + try { + this.state = parseState(await readFile(this.stateFile, "utf8")); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + })(); + await this.loadPromise; + } + + private pruneExpired(): void { + const now = Date.now(); + for (const [hash, token] of Object.entries(this.state.tokens)) { + if (token.expiresAt <= now) { + delete this.state.tokens[hash]; + } + } + } + + private async persist(): Promise { + const directory = path.dirname(this.stateFile); + await mkdir(directory, { recursive: true, mode: 0o700 }); + await chmod(directory, 0o700); + const temporaryFile = `${this.stateFile}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`; + try { + await writeFile(temporaryFile, `${JSON.stringify(this.state, null, 2)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + await rename(temporaryFile, this.stateFile); + } catch (error) { + await unlink(temporaryFile).catch(() => undefined); + throw error; + } + } + + private async mutate(operation: () => T | Promise): Promise { + await this.ensureLoaded(); + const pending = this.mutationQueue.then(async () => { + this.pruneExpired(); + const result = await operation(); + await this.persist(); + return result; + }); + this.mutationQueue = pending.then( + () => undefined, + () => undefined, + ); + return pending; + } + + async getClient(clientId: string): Promise { + await this.ensureLoaded(); + await this.mutationQueue; + return this.state.clients[clientId]; + } + + async registerClient( + client: Omit, + ): Promise { + const supplied = client as Partial; + const registered: OAuthClientInformationFull = { + ...client, + client_id: supplied.client_id || randomUUID(), + client_id_issued_at: supplied.client_id_issued_at || Math.floor(Date.now() / 1000), + }; + return this.mutate(() => { + this.state.clients[registered.client_id] = registered; + return registered; + }); + } + + async issueTokenPair(clientId: string, scopes: string[], resource: string): Promise { + return this.mutate(() => this.issueTokenPairWithoutPersist(clientId, scopes, resource)); + } + + private issueTokenPairWithoutPersist( + clientId: string, + scopes: string[], + resource: string, + ): OAuthTokens { + const accessToken = randomToken(); + const refreshToken = randomToken(); + const now = Date.now(); + this.state.tokens[tokenHash(accessToken)] = { + type: "access", + clientId, + scopes, + expiresAt: now + this.accessTokenTtlSeconds * 1000, + resource, + }; + this.state.tokens[tokenHash(refreshToken)] = { + type: "refresh", + clientId, + scopes, + expiresAt: now + this.refreshTokenTtlSeconds * 1000, + resource, + }; + return { + access_token: accessToken, + token_type: "Bearer", + expires_in: this.accessTokenTtlSeconds, + refresh_token: refreshToken, + scope: scopes.join(" "), + }; + } + + async rotateRefreshToken( + refreshToken: string, + clientId: string, + resource: string, + requestedScopes: string[] | undefined, + ): Promise { + return this.mutate(() => { + const hash = tokenHash(refreshToken); + const current = this.state.tokens[hash]; + if ( + !current || + current.type !== "refresh" || + current.clientId !== clientId || + current.resource !== resource || + current.expiresAt <= Date.now() + ) { + return { status: "invalid" }; + } + const scopes = requestedScopes ?? current.scopes; + if (!scopes.every((scope) => current.scopes.includes(scope))) { + return { status: "invalid_scope" }; + } + delete this.state.tokens[hash]; + return { + status: "ok", + tokens: this.issueTokenPairWithoutPersist(clientId, scopes, resource), + }; + }); + } + + async getAccessToken(token: string): Promise { + await this.ensureLoaded(); + await this.mutationQueue; + const stored = this.state.tokens[tokenHash(token)]; + if (!stored || stored.type !== "access" || stored.expiresAt <= Date.now()) { + return undefined; + } + return stored; + } + + async revoke(token: string, clientId: string): Promise { + await this.mutate(() => { + const hash = tokenHash(token); + if (this.state.tokens[hash]?.clientId === clientId) { + delete this.state.tokens[hash]; + } + }); + } +} + +function escapeHtml(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function hiddenInput(name: string, value: string | undefined): string { + return value === undefined + ? "" + : ``; +} + +function renderAuthorizationPage( + client: OAuthClientInformationFull, + params: AuthorizationParams, + invalidKey: boolean, +): string { + const clientName = client.client_name || "ChatGPT MCP client"; + let redirectHost = params.redirectUri; + try { + redirectHost = new URL(params.redirectUri).host; + } catch { + // The SDK already validates this URL before calling the provider. + } + const fields = [ + hiddenInput("client_id", client.client_id), + hiddenInput("redirect_uri", params.redirectUri), + hiddenInput("response_type", "code"), + hiddenInput("code_challenge", params.codeChallenge), + hiddenInput("code_challenge_method", "S256"), + hiddenInput("scope", params.scopes?.join(" ")), + hiddenInput("state", params.state), + hiddenInput("resource", params.resource?.href), + ].join("\n"); + + return ` + + + + + cokacremote 승인 + + + +
+

cokacremote 연결 승인

+

${escapeHtml(clientName)}이 이 서버의 MCP 도구 사용 권한을 요청했습니다.

+

승인하면 ChatGPT가 이 EC2에서 root 권한으로 명령 실행과 파일 변경을 수행할 수 있습니다.

+ ${invalidKey ? '

인증키가 올바르지 않습니다.

' : ""} +
+ ${fields} + + + +
+ 콜백 대상: ${escapeHtml(redirectHost)} · 범위: ${escapeHtml(params.scopes?.join(" ") || OAUTH_SCOPES.join(" "))} +
+ +`; +} + +export class RemoteDevOAuthProvider implements OAuthServerProvider { + readonly clientsStore: PersistentOAuthStore; + readonly issuerUrl: URL; + readonly resourceUrl: URL; + private readonly authorizationCodes = new Map(); + + constructor(private readonly config: AppConfig) { + if (!config.oauthIssuerUrl || !config.oauthResourceUrl || !config.authToken) { + throw new Error("OAuth configuration is incomplete"); + } + this.issuerUrl = new URL(config.oauthIssuerUrl); + this.resourceUrl = new URL(config.oauthResourceUrl); + this.clientsStore = new PersistentOAuthStore( + config.oauthStateFile, + config.oauthAccessTokenTtlSeconds, + config.oauthRefreshTokenTtlSeconds, + ); + } + + private validateResource(resource: URL | undefined): string { + if (!resource || resource.href !== this.resourceUrl.href) { + throw new InvalidTargetError(`resource must be ${this.resourceUrl.href}`); + } + return resource.href; + } + + private validateScopes(scopes: string[] | undefined): string[] { + const requested = scopes && scopes.length > 0 ? [...new Set(scopes)] : [...OAUTH_SCOPES]; + if (!requested.every((scope) => OAUTH_SCOPES.includes(scope as (typeof OAUTH_SCOPES)[number]))) { + throw new InvalidScopeError("Only the mcp:tools scope is supported"); + } + return requested; + } + + private pruneAuthorizationCodes(): void { + const now = Date.now(); + for (const [code, record] of this.authorizationCodes) { + if (record.expiresAt <= now) { + this.authorizationCodes.delete(code); + } + } + } + + async authorize( + client: OAuthClientInformationFull, + params: AuthorizationParams, + response: Response, + ): Promise { + const resource = this.validateResource(params.resource); + const scopes = this.validateScopes(params.scopes); + const redirectOrigin = new URL(params.redirectUri).origin; + const request = response.req as Request; + const accessKey = + request.method === "POST" && typeof request.body?.access_key === "string" + ? request.body.access_key + : undefined; + + response.set({ + "Content-Security-Policy": + `default-src 'none'; style-src 'unsafe-inline'; form-action 'self' ${redirectOrigin}; base-uri 'none'; frame-ancestors 'none'`, + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + }); + + if (!accessKey || !tokensEqual(accessKey, this.config.authToken!)) { + response + .status(accessKey ? 401 : 200) + .type("html") + .send(renderAuthorizationPage(client, { ...params, scopes, resource: new URL(resource) }, Boolean(accessKey))); + return; + } + + this.pruneAuthorizationCodes(); + const code = randomToken(); + this.authorizationCodes.set(code, { + clientId: client.client_id, + codeChallenge: params.codeChallenge, + redirectUri: params.redirectUri, + resource, + scopes, + expiresAt: Date.now() + this.config.oauthAuthorizationCodeTtlSeconds * 1000, + }); + + const target = new URL(params.redirectUri); + target.searchParams.set("code", code); + if (params.state !== undefined) { + target.searchParams.set("state", params.state); + } + response.redirect(302, target.href); + } + + async challengeForAuthorizationCode( + client: OAuthClientInformationFull, + authorizationCode: string, + ): Promise { + this.pruneAuthorizationCodes(); + const record = this.authorizationCodes.get(authorizationCode); + if (!record || record.clientId !== client.client_id) { + throw new InvalidGrantError("Invalid or expired authorization code"); + } + return record.codeChallenge; + } + + async exchangeAuthorizationCode( + client: OAuthClientInformationFull, + authorizationCode: string, + _codeVerifier?: string, + redirectUri?: string, + resource?: URL, + ): Promise { + this.pruneAuthorizationCodes(); + const record = this.authorizationCodes.get(authorizationCode); + if ( + !record || + record.clientId !== client.client_id || + record.redirectUri !== redirectUri || + record.resource !== this.validateResource(resource) + ) { + throw new InvalidGrantError("Invalid authorization code binding"); + } + this.authorizationCodes.delete(authorizationCode); + return this.clientsStore.issueTokenPair(client.client_id, record.scopes, record.resource); + } + + async exchangeRefreshToken( + client: OAuthClientInformationFull, + refreshToken: string, + scopes?: string[], + resource?: URL, + ): Promise { + const resourceValue = this.validateResource(resource); + const requestedScopes = scopes ? this.validateScopes(scopes) : undefined; + const result = await this.clientsStore.rotateRefreshToken( + refreshToken, + client.client_id, + resourceValue, + requestedScopes, + ); + if (result.status === "invalid_scope") { + throw new InvalidScopeError("Refresh scope exceeds the original grant"); + } + if (result.status === "invalid") { + throw new InvalidGrantError("Invalid or expired refresh token"); + } + return result.tokens; + } + + async verifyAccessToken(token: string): Promise { + const stored = await this.clientsStore.getAccessToken(token); + if (!stored || stored.resource !== this.resourceUrl.href) { + throw new InvalidGrantError("Invalid or expired access token"); + } + return { + token, + clientId: stored.clientId, + scopes: stored.scopes, + expiresAt: Math.floor(stored.expiresAt / 1000), + resource: new URL(stored.resource), + }; + } + + async revokeToken( + client: OAuthClientInformationFull, + request: OAuthTokenRevocationRequest, + ): Promise { + await this.clientsStore.revoke(request.token, client.client_id); + } +} diff --git a/src/paths.ts b/src/paths.ts new file mode 100644 index 0000000..62574f6 --- /dev/null +++ b/src/paths.ts @@ -0,0 +1,13 @@ +import os from "node:os"; +import path from "node:path"; + +export function expandPath(input: string, baseDirectory: string): string { + const expanded = input === "~" + ? os.homedir() + : input.startsWith("~/") + ? path.join(os.homedir(), input.slice(2)) + : input; + return path.isAbsolute(expanded) + ? path.normalize(expanded) + : path.resolve(baseDirectory, expanded); +} diff --git a/src/process-manager.ts b/src/process-manager.ts new file mode 100644 index 0000000..0d0f71a --- /dev/null +++ b/src/process-manager.ts @@ -0,0 +1,499 @@ +import { randomUUID } from "node:crypto"; +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; + +import { errorMessage } from "./errors.js"; + +const OUTPUT_CHUNK_BYTES = 16 * 1024; + +export type ProcessOutputStream = "stdout" | "stderr"; + +interface OutputChunk { + seq: number; + stream: ProcessOutputStream; + data: Buffer; +} + +interface ManagedProcess { + sessionId: string; + child: ChildProcessWithoutNullStreams; + command: string; + cwd: string; + startedAt: number; + endedAt: number | undefined; + exitCode: number | null | undefined; + signal: NodeJS.Signals | null | undefined; + error: string | undefined; + timedOut: boolean; + chunks: OutputChunk[]; + retainedBytes: number; + totalOutputBytes: number; + droppedOutputBytes: number; + nextSeq: number; + waiters: Set<() => void>; + exitWaiters: Set<() => void>; + timeoutHandle: NodeJS.Timeout | undefined; + cleanup: (() => Promise) | undefined; +} + +export interface StartProcessRequest { + executable: string; + args: string[]; + commandForDisplay: string; + cwd: string; + env?: Record | undefined; + timeoutMs?: number | undefined; + stdin?: string | undefined; + cleanup?: (() => Promise) | undefined; +} + +export interface ReadProcessRequest { + afterSeq?: number | undefined; + waitMs?: number | undefined; + maxOutputBytes?: number | undefined; +} + +export interface ProcessReadResult { + sessionId: string; + command: string; + cwd: string; + running: boolean; + pid: number | undefined; + startedAt: string; + endedAt: string | undefined; + wallTimeMs: number; + exitCode: number | null | undefined; + signal: NodeJS.Signals | null | undefined; + timedOut: boolean; + error: string | undefined; + stdout: string; + stderr: string; + output: string; + nextSeq: number; + hasMore: boolean; + totalOutputBytes: number; + droppedOutputBytes: number; +} + +export interface ProcessManagerOptions { + maxRetainedOutputBytes: number; + processRetentionMs: number; + maxProcesses: number; + defaultMaxOutputBytes: number; +} + +export class ProcessManager { + readonly #processes = new Map(); + readonly #options: ProcessManagerOptions; + + constructor(options: ProcessManagerOptions) { + this.#options = options; + } + + start(request: StartProcessRequest): string { + this.prune(); + this.#makeCapacity(); + + const child = spawn(request.executable, request.args, { + cwd: request.cwd, + env: { ...process.env, ...request.env }, + stdio: "pipe", + detached: process.platform !== "win32", + windowsHide: true, + }); + const sessionId = randomUUID(); + const managed: ManagedProcess = { + sessionId, + child, + command: request.commandForDisplay, + cwd: request.cwd, + startedAt: Date.now(), + endedAt: undefined, + exitCode: undefined, + signal: undefined, + error: undefined, + timedOut: false, + chunks: [], + retainedBytes: 0, + totalOutputBytes: 0, + droppedOutputBytes: 0, + nextSeq: 1, + waiters: new Set(), + exitWaiters: new Set(), + timeoutHandle: undefined, + cleanup: request.cleanup, + }; + this.#processes.set(sessionId, managed); + + child.stdout.on("data", (data: Buffer | string) => { + this.#appendOutput(managed, "stdout", Buffer.from(data)); + }); + child.stderr.on("data", (data: Buffer | string) => { + this.#appendOutput(managed, "stderr", Buffer.from(data)); + }); + child.on("error", (error) => { + managed.error = errorMessage(error); + this.#finish(managed, null, null); + }); + child.on("close", (code, signal) => { + this.#finish(managed, code, signal); + }); + + const timeoutMs = request.timeoutMs ?? 0; + if (timeoutMs > 0) { + managed.timeoutHandle = setTimeout(() => { + managed.timedOut = true; + managed.error ??= `Process exceeded timeout of ${timeoutMs} ms`; + this.#signal(managed, "SIGTERM"); + const forceTimer = setTimeout(() => { + if (this.#isRunning(managed)) { + this.#signal(managed, "SIGKILL"); + } + }, 5000); + forceTimer.unref(); + }, timeoutMs); + managed.timeoutHandle.unref(); + } + + if (request.stdin !== undefined && request.stdin.length > 0) { + child.stdin.write(request.stdin); + } + return sessionId; + } + + async read( + sessionId: string, + request: ReadProcessRequest = {}, + ): Promise { + const managed = this.#require(sessionId); + const afterSeq = Math.max(0, request.afterSeq ?? 0); + const waitMs = Math.max(0, request.waitMs ?? 0); + if (waitMs > 0) { + await this.#waitForOutput(managed, afterSeq, waitMs); + } + + const maxOutputBytes = Math.max( + OUTPUT_CHUNK_BYTES, + Math.min( + request.maxOutputBytes ?? this.#options.defaultMaxOutputBytes, + this.#options.defaultMaxOutputBytes, + ), + ); + const eligible = managed.chunks.filter((chunk) => chunk.seq > afterSeq); + const selected: OutputChunk[] = []; + let selectedBytes = 0; + for (const chunk of eligible) { + if (selectedBytes + chunk.data.length > maxOutputBytes) { + break; + } + selected.push(chunk); + selectedBytes += chunk.data.length; + } + + const stdout = Buffer.concat( + selected.filter((chunk) => chunk.stream === "stdout").map((chunk) => chunk.data), + ).toString("utf8"); + const stderr = Buffer.concat( + selected.filter((chunk) => chunk.stream === "stderr").map((chunk) => chunk.data), + ).toString("utf8"); + const output = Buffer.concat(selected.map((chunk) => chunk.data)).toString("utf8"); + const nextSeq = selected.at(-1)?.seq ?? afterSeq; + const now = managed.endedAt ?? Date.now(); + + return { + sessionId, + command: managed.command, + cwd: managed.cwd, + running: this.#isRunning(managed), + pid: managed.child.pid, + startedAt: new Date(managed.startedAt).toISOString(), + endedAt: + managed.endedAt === undefined + ? undefined + : new Date(managed.endedAt).toISOString(), + wallTimeMs: now - managed.startedAt, + exitCode: managed.exitCode, + signal: managed.signal, + timedOut: managed.timedOut, + error: managed.error, + stdout, + stderr, + output, + nextSeq, + hasMore: eligible.length > selected.length, + totalOutputBytes: managed.totalOutputBytes, + droppedOutputBytes: managed.droppedOutputBytes, + }; + } + + async write( + sessionId: string, + input: string, + closeStdin = false, + ): Promise { + const managed = this.#require(sessionId); + if (input.length === 0 && !closeStdin) { + return; + } + if (!this.#isRunning(managed)) { + throw new Error(`Process ${sessionId} is not running`); + } + if (managed.child.stdin.destroyed || !managed.child.stdin.writable) { + throw new Error(`stdin is closed for process ${sessionId}`); + } + + if (input.length > 0) { + await new Promise((resolve, reject) => { + managed.child.stdin.write(input, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + if (closeStdin) { + managed.child.stdin.end(); + } + } + + async waitForExit(sessionId: string, waitMs: number): Promise { + const managed = this.#require(sessionId); + if (!this.#isRunning(managed) || waitMs <= 0) { + return; + } + await new Promise((resolve) => { + let settled = false; + const finish = () => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + managed.exitWaiters.delete(finish); + resolve(); + }; + const timer = setTimeout(finish, waitMs); + managed.exitWaiters.add(finish); + if (!this.#isRunning(managed)) { + finish(); + } + }); + } + + async terminate( + sessionId: string, + signal: NodeJS.Signals = "SIGTERM", + graceMs = 3000, + ): Promise { + const managed = this.#require(sessionId); + if (this.#isRunning(managed)) { + this.#signal(managed, signal); + if (signal !== "SIGKILL" && graceMs > 0) { + const forceTimer = setTimeout(() => { + if (this.#isRunning(managed)) { + this.#signal(managed, "SIGKILL"); + } + }, graceMs); + forceTimer.unref(); + } + } + return this.read(sessionId, { waitMs: Math.min(graceMs, 1000) }); + } + + list(): Array<{ + sessionId: string; + pid: number | undefined; + command: string; + cwd: string; + running: boolean; + startedAt: string; + endedAt: string | undefined; + exitCode: number | null | undefined; + }> { + this.prune(); + return [...this.#processes.values()].map((managed) => ({ + sessionId: managed.sessionId, + pid: managed.child.pid, + command: managed.command, + cwd: managed.cwd, + running: this.#isRunning(managed), + startedAt: new Date(managed.startedAt).toISOString(), + endedAt: + managed.endedAt === undefined + ? undefined + : new Date(managed.endedAt).toISOString(), + exitCode: managed.exitCode, + })); + } + + prune(): void { + const cutoff = Date.now() - this.#options.processRetentionMs; + for (const [sessionId, managed] of this.#processes) { + if (managed.endedAt !== undefined && managed.endedAt < cutoff) { + this.#processes.delete(sessionId); + } + } + } + + async shutdown(): Promise { + const running = [...this.#processes.values()].filter((managed) => + this.#isRunning(managed), + ); + for (const managed of running) { + this.#signal(managed, "SIGTERM"); + } + await new Promise((resolve) => setTimeout(resolve, running.length > 0 ? 500 : 0)); + for (const managed of running) { + if (this.#isRunning(managed)) { + this.#signal(managed, "SIGKILL"); + } + } + } + + #makeCapacity(): void { + if (this.#processes.size < this.#options.maxProcesses) { + return; + } + const completed = [...this.#processes.values()] + .filter((managed) => managed.endedAt !== undefined) + .sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0)); + while ( + this.#processes.size >= this.#options.maxProcesses && + completed.length > 0 + ) { + const managed = completed.shift(); + if (managed) { + this.#processes.delete(managed.sessionId); + } + } + if (this.#processes.size >= this.#options.maxProcesses) { + throw new Error( + `Maximum managed process count (${this.#options.maxProcesses}) reached`, + ); + } + } + + #require(sessionId: string): ManagedProcess { + const managed = this.#processes.get(sessionId); + if (!managed) { + throw new Error(`Unknown process session: ${sessionId}`); + } + return managed; + } + + #appendOutput( + managed: ManagedProcess, + stream: ProcessOutputStream, + data: Buffer, + ): void { + for (let offset = 0; offset < data.length; offset += OUTPUT_CHUNK_BYTES) { + const chunkData = Buffer.from(data.subarray(offset, offset + OUTPUT_CHUNK_BYTES)); + managed.chunks.push({ + seq: managed.nextSeq, + stream, + data: chunkData, + }); + managed.nextSeq += 1; + managed.retainedBytes += chunkData.length; + managed.totalOutputBytes += chunkData.length; + } + while ( + managed.retainedBytes > this.#options.maxRetainedOutputBytes && + managed.chunks.length > 0 + ) { + const removed = managed.chunks.shift(); + if (removed) { + managed.retainedBytes -= removed.data.length; + managed.droppedOutputBytes += removed.data.length; + } + } + this.#notify(managed); + } + + #finish( + managed: ManagedProcess, + code: number | null, + signal: NodeJS.Signals | null, + ): void { + if (managed.endedAt !== undefined) { + return; + } + managed.endedAt = Date.now(); + managed.exitCode = code; + managed.signal = signal; + if (managed.timeoutHandle) { + clearTimeout(managed.timeoutHandle); + managed.timeoutHandle = undefined; + } + this.#notify(managed); + const exitWaiters = [...managed.exitWaiters]; + managed.exitWaiters.clear(); + for (const waiter of exitWaiters) { + waiter(); + } + if (managed.cleanup) { + void managed.cleanup().catch((error) => { + managed.error ??= `Cleanup failed: ${errorMessage(error)}`; + }); + } + } + + #notify(managed: ManagedProcess): void { + const waiters = [...managed.waiters]; + managed.waiters.clear(); + for (const waiter of waiters) { + waiter(); + } + } + + #waitForOutput( + managed: ManagedProcess, + afterSeq: number, + waitMs: number, + ): Promise { + if (managed.nextSeq - 1 > afterSeq || !this.#isRunning(managed)) { + return Promise.resolve(); + } + return new Promise((resolve) => { + let settled = false; + const finish = () => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + managed.waiters.delete(finish); + resolve(); + }; + const timer = setTimeout(finish, waitMs); + managed.waiters.add(finish); + if (managed.nextSeq - 1 > afterSeq || !this.#isRunning(managed)) { + finish(); + } + }); + } + + #isRunning(managed: ManagedProcess): boolean { + return managed.endedAt === undefined; + } + + #signal(managed: ManagedProcess, signal: NodeJS.Signals): void { + const pid = managed.child.pid; + if (pid === undefined) { + return; + } + try { + if (process.platform !== "win32") { + process.kill(-pid, signal); + } else { + managed.child.kill(signal); + } + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ESRCH") { + managed.error ??= `Failed to signal process: ${errorMessage(error)}`; + } + } + } +} diff --git a/src/script-runner.ts b/src/script-runner.ts new file mode 100644 index 0000000..4207e77 --- /dev/null +++ b/src/script-runner.ts @@ -0,0 +1,107 @@ +import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import type { ProcessReadResult } from "./process-manager.js"; +import { ProcessManager } from "./process-manager.js"; + +export type ScriptRuntime = "bash" | "sh" | "node" | "python" | "custom"; + +export interface RunScriptRequest { + runtime: ScriptRuntime; + script: string; + cwd: string; + args?: string[] | undefined; + env?: Record | undefined; + interpreter?: string | undefined; + interpreterArgs?: string[] | undefined; + timeoutMs?: number | undefined; + yieldTimeMs?: number | undefined; + maxOutputBytes?: number | undefined; + stdin?: string | undefined; + keepScript?: boolean | undefined; +} + +export interface RunScriptResult extends ProcessReadResult { + scriptPath: string | undefined; +} + +interface RuntimeDefinition { + executable: string; + extension: string; +} + +function runtimeDefinition(request: RunScriptRequest): RuntimeDefinition { + if (request.runtime === "custom") { + if (!request.interpreter?.trim()) { + throw new Error("interpreter is required when runtime is custom"); + } + return { + executable: request.interpreter, + extension: ".script", + }; + } + + const definitions: Record, RuntimeDefinition> = { + bash: { executable: request.interpreter || "bash", extension: ".sh" }, + sh: { executable: request.interpreter || "sh", extension: ".sh" }, + node: { executable: request.interpreter || process.execPath, extension: ".mjs" }, + python: { executable: request.interpreter || "python3", extension: ".py" }, + }; + return definitions[request.runtime]; +} + +function displayCommand(executable: string, args: string[]): string { + return [executable, ...args].map((value) => JSON.stringify(value)).join(" "); +} + +export async function runScript( + processManager: ProcessManager, + request: RunScriptRequest, +): Promise { + const runtime = runtimeDefinition(request); + const temporaryDirectory = await mkdtemp( + path.join(os.tmpdir(), "remote-dev-mcp-script-"), + ); + const scriptPath = path.join(temporaryDirectory, `script${runtime.extension}`); + await writeFile(scriptPath, request.script, { mode: 0o700 }); + await chmod(scriptPath, 0o700); + + const processArgs = [ + ...(request.interpreterArgs ?? []), + scriptPath, + ...(request.args ?? []), + ]; + const keepScript = request.keepScript ?? false; + const cleanup = keepScript + ? undefined + : async () => { + await rm(temporaryDirectory, { recursive: true, force: true }); + }; + + let sessionId: string; + try { + sessionId = processManager.start({ + executable: runtime.executable, + args: processArgs, + commandForDisplay: displayCommand(runtime.executable, processArgs), + cwd: request.cwd, + env: request.env, + timeoutMs: request.timeoutMs, + stdin: request.stdin, + cleanup, + }); + } catch (error) { + await rm(temporaryDirectory, { recursive: true, force: true }); + throw error; + } + + await processManager.waitForExit(sessionId, request.yieldTimeMs ?? 10_000); + const result = await processManager.read(sessionId, { + maxOutputBytes: request.maxOutputBytes, + }); + return { + ...result, + scriptPath: keepScript ? scriptPath : undefined, + }; +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..d7e8a4c --- /dev/null +++ b/src/server.ts @@ -0,0 +1,48 @@ +import { loadConfig } from "./config.js"; +import { errorMessage } from "./errors.js"; +import { startHttpServer } from "./http-server.js"; +import { createServices } from "./mcp-server.js"; + +async function main(): Promise { + const config = loadConfig(); + const services = createServices(config); + const running = await startHttpServer(config, services); + const endpointUrl = config.publicUrl + ? `${config.publicUrl}${config.endpoint}` + : `http://${config.host}:${config.port}${config.endpoint}`; + + console.log(`cokacremote listening at ${endpointUrl}`); + console.log(`default cwd: ${config.defaultCwd}`); + console.log("execution mode: unrestricted host access"); + console.log( + config.allowNoAuth && !config.authToken + ? "authentication: disabled" + : config.oauthEnabled + ? "authentication: static bearer + OAuth 2.1 (DCR/PKCE)" + : "authentication: bearer token", + ); + + let shuttingDown = false; + const shutdown = async (signal: string): Promise => { + if (shuttingDown) { + return; + } + shuttingDown = true; + console.log(`received ${signal}; shutting down`); + try { + await running.close(); + process.exitCode = 0; + } catch (error) { + console.error("shutdown failed:", errorMessage(error)); + process.exitCode = 1; + } + }; + + process.on("SIGINT", () => void shutdown("SIGINT")); + process.on("SIGTERM", () => void shutdown("SIGTERM")); +} + +main().catch((error) => { + console.error("cokacremote failed to start:", errorMessage(error)); + process.exitCode = 1; +}); diff --git a/src/tool-result.ts b/src/tool-result.ts new file mode 100644 index 0000000..1705ee4 --- /dev/null +++ b/src/tool-result.ts @@ -0,0 +1,29 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +import { errorMessage } from "./errors.js"; + +export function successResult(data: Record): CallToolResult { + return { + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + structuredContent: data, + }; +} + +export function errorResult(error: unknown): CallToolResult { + const data = { error: errorMessage(error) }; + return { + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + structuredContent: data, + isError: true, + }; +} + +export async function runTool( + operation: () => Promise> | Record, +): Promise { + try { + return successResult(await operation()); + } catch (error) { + return errorResult(error); + } +} diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..6837b83 --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; + +import { loadConfig } from "../src/config.js"; + +describe("loadConfig", () => { + it("requires authentication unless explicitly disabled", () => { + expect(() => loadConfig({}, "/tmp")).toThrow("MCP_AUTH_TOKEN is required"); + expect(loadConfig({ MCP_ALLOW_NO_AUTH: "true" }, "/tmp").allowNoAuth).toBe(true); + }); + + it("loads full-access host settings", () => { + const config = loadConfig( + { + MCP_AUTH_TOKEN: "secret", + MCP_PORT: "4321", + MCP_DEFAULT_CWD: "/", + MCP_ALLOWED_HOSTS: "mcp.example.com,localhost", + }, + "/tmp", + ); + + expect(config).toMatchObject({ + port: 4321, + defaultCwd: "/", + authToken: "secret", + allowedHosts: ["mcp.example.com", "localhost"], + }); + }); + + it("requires public HTTPS metadata when OAuth is enabled", () => { + expect(() => + loadConfig({ MCP_AUTH_TOKEN: "secret", MCP_OAUTH_ENABLED: "true" }, "/tmp"), + ).toThrow("MCP_OAUTH_ISSUER is required"); + + const config = loadConfig( + { + MCP_AUTH_TOKEN: "secret", + MCP_OAUTH_ENABLED: "true", + MCP_PUBLIC_URL: "https://mcp.example.com", + MCP_OAUTH_STATE_FILE: "/tmp/oauth-state.json", + }, + "/tmp", + ); + expect(config).toMatchObject({ + oauthEnabled: true, + oauthIssuerUrl: "https://mcp.example.com/", + oauthResourceUrl: "https://mcp.example.com/mcp", + oauthStateFile: "/tmp/oauth-state.json", + }); + }); +}); diff --git a/test/file-service.test.ts b/test/file-service.test.ts new file mode 100644 index 0000000..75ea22d --- /dev/null +++ b/test/file-service.test.ts @@ -0,0 +1,130 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { FileService } from "../src/file-service.js"; + +describe("FileService", () => { + let temporaryDirectory: string; + let files: FileService; + + beforeEach(async () => { + temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "remote-dev-mcp-test-")); + files = new FileService({ + defaultCwd: temporaryDirectory, + maxChunkBytes: 1024 * 1024, + maxEditFileBytes: 1024 * 1024, + maxOutputBytes: 1024 * 1024, + }); + }); + + afterEach(async () => { + await rm(temporaryDirectory, { recursive: true, force: true }); + }); + + it("writes, reads, lists, and replaces text", async () => { + await files.writeFileContent( + "src/example.txt", + undefined, + "alpha beta\n", + "utf8", + "overwrite", + true, + ); + await files.replaceInFile( + "src/example.txt", + undefined, + "beta", + "gamma", + false, + 1, + ); + + const read = await files.readFileChunk( + "src/example.txt", + undefined, + 0, + 1024, + "utf8", + ); + const listed = await files.listDirectory(".", undefined, { + recursive: true, + includeMetadata: true, + }); + + expect(read.content).toBe("alpha gamma\n"); + expect(read.eof).toBe(true); + expect(listed.entries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ relativePath: path.join("src", "example.txt") }), + ]), + ); + }); + + it("uploads and downloads binary chunks with offsets", async () => { + const first = await files.uploadChunk( + "artifact.bin", + undefined, + Buffer.from("hello").toString("base64"), + 0, + true, + true, + ); + await files.uploadChunk( + "artifact.bin", + undefined, + Buffer.from(" world").toString("base64"), + first.nextOffset as number, + false, + true, + ); + + const downloaded = await files.downloadChunk( + "artifact.bin", + undefined, + 0, + 1024, + ); + const hashed = await files.hashFile("artifact.bin", undefined, "sha256"); + + expect(Buffer.from(downloaded.dataBase64 as string, "base64").toString()).toBe( + "hello world", + ); + expect(downloaded.eof).toBe(true); + expect(hashed.digest).toBe( + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ); + }); + + it("validates and applies a unified diff", async () => { + await writeFile(path.join(temporaryDirectory, "patch.txt"), "old\n", "utf8"); + const patchText = [ + "diff --git a/patch.txt b/patch.txt", + "--- a/patch.txt", + "+++ b/patch.txt", + "@@ -1 +1 @@", + "-old", + "+new", + "", + ].join("\n"); + + const checked = await files.applyPatch(patchText, undefined, { + checkOnly: true, + reverse: false, + threeWay: false, + }); + const applied = await files.applyPatch(patchText, undefined, { + checkOnly: false, + reverse: false, + threeWay: false, + }); + + expect(checked.applied).toBe(false); + expect(applied.applied).toBe(true); + expect(await readFile(path.join(temporaryDirectory, "patch.txt"), "utf8")).toBe( + "new\n", + ); + }); +}); diff --git a/test/mcp.integration.test.ts b/test/mcp.integration.test.ts new file mode 100644 index 0000000..63e5738 --- /dev/null +++ b/test/mcp.integration.test.ts @@ -0,0 +1,124 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import type { AddressInfo } from "node:net"; +import os from "node:os"; +import path from "node:path"; + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { loadConfig, type AppConfig } from "../src/config.js"; +import { startHttpServer, type RunningHttpServer } from "../src/http-server.js"; +import { createServices, type McpServices } from "../src/mcp-server.js"; + +describe("remote development MCP server", () => { + let temporaryDirectory: string; + let config: AppConfig; + let services: McpServices; + let running: RunningHttpServer; + let endpoint: URL; + + beforeAll(async () => { + temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "remote-dev-mcp-http-test-")); + config = loadConfig( + { + MCP_AUTH_TOKEN: "integration-secret", + MCP_HOST: "127.0.0.1", + MCP_DEFAULT_CWD: temporaryDirectory, + MCP_MAX_FILE_CHUNK_BYTES: "65536", + }, + temporaryDirectory, + ); + config.port = 0; + services = createServices(config); + running = await startHttpServer(config, services); + const address = running.httpServer.address() as AddressInfo; + endpoint = new URL(`http://127.0.0.1:${address.port}${config.endpoint}`); + }); + + afterAll(async () => { + await running.close(); + await rm(temporaryDirectory, { recursive: true, force: true }); + }); + + it("rejects unauthenticated MCP initialization", async () => { + const response = await fetch(endpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + }), + }); + + expect(response.status).toBe(401); + }); + + it("lists tools and executes script and file workflows", async () => { + const client = new Client({ name: "integration-test", version: "1.0.0" }); + const transport = new StreamableHTTPClientTransport(endpoint, { + requestInit: { + headers: { Authorization: "Bearer integration-secret" }, + }, + }); + await client.connect(transport); + try { + expect(client.getServerVersion()).toMatchObject({ + name: "cokacremote", + version: "0.1.0", + }); + const tools = await client.listTools(); + expect(tools.tools.map((tool) => tool.name)).toEqual( + expect.arrayContaining([ + "exec_command", + "run_script", + "write_stdin", + "read_file", + "write_file", + "apply_patch", + "upload_file", + "download_file", + ]), + ); + + const scriptResult = await client.callTool({ + name: "run_script", + arguments: { + runtime: "node", + script: "console.log(6 * 7)", + yieldTimeMs: 2000, + }, + }); + expect(scriptResult.isError).not.toBe(true); + expect(scriptResult.structuredContent).toMatchObject({ + completed: true, + exitCode: 0, + stdout: "42\n", + }); + + const writeResult = await client.callTool({ + name: "write_file", + arguments: { path: "hello.txt", content: "hello MCP\n" }, + }); + expect(writeResult.isError).not.toBe(true); + + const readResult = await client.callTool({ + name: "read_file", + arguments: { path: "hello.txt" }, + }); + expect(readResult.structuredContent).toMatchObject({ + content: "hello MCP\n", + eof: true, + }); + } finally { + await transport.terminateSession(); + await client.close(); + } + }); +}); diff --git a/test/oauth.integration.test.ts b/test/oauth.integration.test.ts new file mode 100644 index 0000000..6d4d467 --- /dev/null +++ b/test/oauth.integration.test.ts @@ -0,0 +1,249 @@ +import { createHash, randomBytes } from "node:crypto"; +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import type { AddressInfo } from "node:net"; +import { createServer } from "node:net"; +import os from "node:os"; +import path from "node:path"; + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { loadConfig, type AppConfig } from "../src/config.js"; +import { startHttpServer, type RunningHttpServer } from "../src/http-server.js"; +import { createServices } from "../src/mcp-server.js"; + +async function reservePort(): Promise { + const server = createServer(); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const port = (server.address() as AddressInfo).port; + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + return port; +} + +function form(values: Record): URLSearchParams { + return new URLSearchParams(values); +} + +describe("OAuth 2.1 MCP authorization", () => { + let temporaryDirectory: string; + let stateFile: string; + let config: AppConfig; + let running: RunningHttpServer; + let baseUrl: string; + let resourceUrl: string; + + beforeAll(async () => { + temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "remote-dev-mcp-oauth-test-")); + stateFile = path.join(temporaryDirectory, "oauth", "state.json"); + const port = await reservePort(); + baseUrl = `http://127.0.0.1:${port}`; + resourceUrl = `${baseUrl}/mcp`; + config = loadConfig( + { + MCP_AUTH_TOKEN: "oauth-login-secret", + MCP_OAUTH_ENABLED: "true", + MCP_PUBLIC_URL: baseUrl, + MCP_OAUTH_STATE_FILE: stateFile, + MCP_HOST: "127.0.0.1", + MCP_PORT: String(port), + MCP_DEFAULT_CWD: temporaryDirectory, + }, + temporaryDirectory, + ); + running = await startHttpServer(config, createServices(config)); + }); + + afterAll(async () => { + await running.close(); + await rm(temporaryDirectory, { recursive: true, force: true }); + }); + + it("discovers, authorizes with PKCE, refreshes, revokes, and calls MCP tools", async () => { + const unauthenticated = await fetch(resourceUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "oauth-test", version: "1" }, + }, + }), + }); + expect(unauthenticated.status).toBe(401); + expect(unauthenticated.headers.get("www-authenticate")).toContain( + `${baseUrl}/.well-known/oauth-protected-resource/mcp`, + ); + + for (const metadataPath of [ + "/.well-known/oauth-protected-resource", + "/.well-known/oauth-protected-resource/mcp", + ]) { + const response = await fetch(`${baseUrl}${metadataPath}`); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + resource: resourceUrl, + authorization_servers: [`${baseUrl}/`], + scopes_supported: ["mcp:tools"], + resource_name: "cokacremote", + }); + } + + const metadataResponse = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`); + expect(metadataResponse.status).toBe(200); + expect(await metadataResponse.json()).toMatchObject({ + issuer: `${baseUrl}/`, + authorization_endpoint: `${baseUrl}/authorize`, + token_endpoint: `${baseUrl}/token`, + registration_endpoint: `${baseUrl}/register`, + code_challenge_methods_supported: ["S256"], + token_endpoint_auth_methods_supported: expect.arrayContaining(["none"]), + }); + + const redirectUri = "https://chatgpt.com/connector/oauth/test-callback"; + const registrationResponse = await fetch(`${baseUrl}/register`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + redirect_uris: [redirectUri], + token_endpoint_auth_method: "none", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + client_name: "ChatGPT OAuth integration test", + scope: "mcp:tools", + }), + }); + expect(registrationResponse.status).toBe(201); + const registered = (await registrationResponse.json()) as { client_id: string }; + expect(registered.client_id).toBeTruthy(); + + const codeVerifier = randomBytes(48).toString("base64url"); + const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url"); + const authorizationValues = { + client_id: registered.client_id, + redirect_uri: redirectUri, + response_type: "code", + code_challenge: codeChallenge, + code_challenge_method: "S256", + scope: "mcp:tools", + state: "oauth-test-state", + resource: resourceUrl, + }; + + const loginPage = await fetch(`${baseUrl}/authorize?${form(authorizationValues)}`, { + redirect: "manual", + }); + expect(loginPage.status).toBe(200); + expect(loginPage.headers.get("content-security-policy")).toContain( + "form-action 'self' https://chatgpt.com", + ); + expect(await loginPage.text()).toContain("MCP 인증키"); + + const rejectedLogin = await fetch(`${baseUrl}/authorize`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: form({ ...authorizationValues, access_key: "wrong-key" }), + redirect: "manual", + }); + expect(rejectedLogin.status).toBe(401); + + const approvedLogin = await fetch(`${baseUrl}/authorize`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: form({ ...authorizationValues, access_key: "oauth-login-secret" }), + redirect: "manual", + }); + expect(approvedLogin.status).toBe(302); + const callback = new URL(approvedLogin.headers.get("location")!); + expect(callback.origin + callback.pathname).toBe(redirectUri); + expect(callback.searchParams.get("state")).toBe("oauth-test-state"); + const authorizationCode = callback.searchParams.get("code"); + expect(authorizationCode).toBeTruthy(); + + const tokenResponse = await fetch(`${baseUrl}/token`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: form({ + grant_type: "authorization_code", + client_id: registered.client_id, + code: authorizationCode!, + code_verifier: codeVerifier, + redirect_uri: redirectUri, + resource: resourceUrl, + }), + }); + expect(tokenResponse.status).toBe(200); + const tokens = (await tokenResponse.json()) as { + access_token: string; + refresh_token: string; + expires_in: number; + scope: string; + }; + expect(tokens).toMatchObject({ expires_in: 3600, scope: "mcp:tools" }); + + await running.close(); + running = await startHttpServer(config, createServices(config)); + + const client = new Client({ name: "oauth-integration-test", version: "1.0.0" }); + const transport = new StreamableHTTPClientTransport(new URL(resourceUrl), { + requestInit: { headers: { Authorization: `Bearer ${tokens.access_token}` } }, + }); + await client.connect(transport); + try { + const tools = await client.listTools(); + expect(tools.tools.some((tool) => tool.name === "run_script")).toBe(true); + } finally { + await transport.terminateSession(); + await client.close(); + } + + const refreshResponse = await fetch(`${baseUrl}/token`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: form({ + grant_type: "refresh_token", + client_id: registered.client_id, + refresh_token: tokens.refresh_token, + resource: resourceUrl, + }), + }); + expect(refreshResponse.status).toBe(200); + const refreshed = (await refreshResponse.json()) as { + access_token: string; + refresh_token: string; + }; + expect(refreshed.access_token).not.toBe(tokens.access_token); + expect(refreshed.refresh_token).not.toBe(tokens.refresh_token); + + const revokeResponse = await fetch(`${baseUrl}/revoke`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: form({ client_id: registered.client_id, token: refreshed.access_token }), + }); + expect(revokeResponse.status).toBe(200); + + const revokedRequest = await fetch(resourceUrl, { + method: "POST", + headers: { + authorization: `Bearer ${refreshed.access_token}`, + "content-type": "application/json", + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "initialize", params: {} }), + }); + expect(revokedRequest.status).toBe(401); + + expect((await stat(stateFile)).mode & 0o777).toBe(0o600); + const persisted = await readFile(stateFile, "utf8"); + expect(persisted).not.toContain(tokens.access_token); + expect(persisted).not.toContain(tokens.refresh_token); + }); +}); diff --git a/test/process-manager.test.ts b/test/process-manager.test.ts new file mode 100644 index 0000000..cfa698b --- /dev/null +++ b/test/process-manager.test.ts @@ -0,0 +1,79 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { ProcessManager } from "../src/process-manager.js"; + +function createManager(): ProcessManager { + return new ProcessManager({ + maxRetainedOutputBytes: 1024 * 1024, + processRetentionMs: 60_000, + maxProcesses: 16, + defaultMaxOutputBytes: 1024 * 1024, + }); +} + +describe("ProcessManager", () => { + let manager: ProcessManager | undefined; + + afterEach(async () => { + await manager?.shutdown(); + }); + + it("captures stdout, stderr, and exit state", async () => { + manager = createManager(); + const sessionId = manager.start({ + executable: "/bin/bash", + args: ["-c", "printf stdout; printf stderr >&2"], + commandForDisplay: "test output", + cwd: process.cwd(), + }); + + await manager.waitForExit(sessionId, 2000); + const result = await manager.read(sessionId); + + expect(result).toMatchObject({ + running: false, + exitCode: 0, + stdout: "stdout", + stderr: "stderr", + timedOut: false, + }); + expect(result.output).toContain("stdout"); + expect(result.output).toContain("stderr"); + }); + + it("supports interactive stdin and closes cleanly", async () => { + manager = createManager(); + const sessionId = manager.start({ + executable: "/bin/cat", + args: [], + commandForDisplay: "cat", + cwd: process.cwd(), + }); + + await manager.write(sessionId, "hello\n", true); + await manager.waitForExit(sessionId, 2000); + const result = await manager.read(sessionId); + + expect(result.running).toBe(false); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe("hello\n"); + }); + + it("terminates a command when its timeout expires", async () => { + manager = createManager(); + const sessionId = manager.start({ + executable: "/bin/bash", + args: ["-c", "sleep 10"], + commandForDisplay: "sleep 10", + cwd: process.cwd(), + timeoutMs: 50, + }); + + await manager.waitForExit(sessionId, 3000); + const result = await manager.read(sessionId); + + expect(result.running).toBe(false); + expect(result.timedOut).toBe(true); + expect(result.error).toContain("timeout"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c0fd8bd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": ".", + "outDir": "dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true, + "declaration": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["dist", "node_modules", "codex"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..9b56e79 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + exclude: ["codex/**", "node_modules/**", "dist/**"], + testTimeout: 10_000, + hookTimeout: 10_000, + }, +});