diff --git a/README-STABILITY.md b/README-STABILITY.md new file mode 100644 index 00000000..90bcbce7 --- /dev/null +++ b/README-STABILITY.md @@ -0,0 +1,46 @@ +# Browser MCP stability fix (0.2.1) + +## Problem + +Cursor restarts the MCP server process often. The old server called `killProcessOnPort(9009)` on every start, which killed the WebSocket the browser extension was connected to. That caused random `Not connected` errors. + +## Fix + +1. **Persistent bridge daemon** (`dist/ws-daemon.js`) keeps the extension WebSocket alive on port `9009`. +2. **MCP control channel** on port `9010` lets Cursor reconnect without dropping the browser tab. +3. **WebSocket ping keepalive** every 25s prevents idle disconnects. +4. **No port killing** on startup. +5. **Bridge token auth** on the control channel (`~/.browsermcp/bridge.token`, mode `0600`) blocks other local processes from driving your browser. +6. **Input validation** blocks `javascript:`, `file:`, and `data:` navigation URLs and caps wait/text payload sizes. + +The MCP server auto-starts the daemon if it is not already running. + +## One-click pairing + +Install Browser MCP in your editor and get Cursor / VS Code deeplinks: + +```bash +npx @browsermcp/mcp pair --open cursor +``` + +Or from a local build: + +```bash +node dist/index.js pair --open cursor +``` + +This writes `browsermcp` to `~/.cursor/mcp.json` (and other supported clients), starts the bridge daemon, and prints one-click install links. + +After pairing: **Cursor Settings → MCP → restart browsermcp**. + +The MCP server auto-restarts a stale bridge daemon if the control port is occupied but authentication fails (for example after upgrading to token auth). + +Then in Chrome: open the Browser MCP extension → **Connect** on your Cloudflare tab. + +## Manual daemon (optional) + +```bash +node /Users/misha/nocursor/browsermcp/mcp/dist/ws-daemon.js +``` + +Ports: extension `9009`, MCP bridge `9010`. diff --git a/README.md b/README.md index a85af20d..95e8cc4d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,87 @@ - - Browser MCP banner - +# Browser MCP (community fork) -

Browser MCP

+A maintained fork of [Browser MCP](https://github.com/BrowserMCP/mcp), the MCP server and Chrome extension that lets AI tools automate your existing browser. -

- Automate your browser with AI. -
- Website - • - Docs -

+Upstream development has slowed, so this repository exists to keep the project usable, fix regressions, and ship improvements without waiting on the original maintainers. ## About -Browser MCP is an MCP server + Chrome extension that allows you to automate your browser using AI applications like VS Code, Claude, Cursor, and Windsurf. +Browser MCP connects an MCP server to a Chrome extension so applications like Cursor, VS Code, Claude, and Windsurf can drive the browser you already use: your profile, your logins, your tabs. + +This fork keeps that model and focuses on reliability for day-to-day agent workflows. + +## Why this fork + +- Upstream activity and issue response have been limited for some time. +- Cursor and other clients restart MCP processes frequently; the original server could drop the extension connection on every restart. +- We wanted standalone builds, clearer pairing, and hardening without depending on upstream release cadence. + +If upstream becomes active again, we are happy to contribute fixes back. Until then, this repo is the maintained line. + +## What is different here + +Compared to upstream `0.1.x`, this fork (`0.2.x`) includes: + +- **Persistent bridge daemon** — keeps the extension WebSocket alive across MCP restarts. +- **Separate control channel** — the editor reconnects without killing the browser tab connection. +- **No port killing on startup** — avoids random `Not connected` errors. +- **Bridge token auth** — local control traffic requires a token in `~/.browsermcp/bridge.token`. +- **Input validation** — blocks dangerous navigation schemes and caps payload sizes. +- **One-click pairing** — `browsermcp pair` writes client config and starts the daemon. +- **Standalone build** — ships a self-contained `dist/` without fragile monorepo assumptions. + +See [README-STABILITY.md](./README-STABILITY.md) for implementation details and pairing steps. + +The [`extension/`](../extension/) directory tracks popup UI fixes (for example, disconnecting from any tab). See [extension/README.md](../extension/README.md). ## Features -- ⚡ Fast: Automation happens locally on your machine, resulting in better performance without network latency. -- 🔒 Private: Since automation happens locally, your browser activity stays on your device and isn't sent to remote servers. -- 👤 Logged In: Uses your existing browser profile, keeping you logged into all your services. -- 🥷🏼 Stealth: Avoids basic bot detection and CAPTCHAs by using your real browser fingerprint. +- **Fast** — automation runs locally; no remote browser latency. +- **Private** — browser activity stays on your machine. +- **Logged in** — uses your existing profile and sessions. +- **Stealth** — uses your real browser fingerprint to reduce basic bot detection. + +## Quick start + +From a local build: -## Contributing +```bash +npm install +npm run build +node dist/index.js pair --open cursor +``` -This repo contains all the core MCP code for Browser MCP, but currently cannot yet be built on its own due to dependencies on utils and types from the monorepo where it's developed. +After pairing, restart the `browsermcp` MCP server in your editor. In Chrome, open the Browser MCP extension and click **Connect** on the tab you want to automate. + +Published installs can use: + +```bash +npx @browsermcp/mcp pair --open cursor +``` + +## Development + +Requires Node.js 18+. + +```bash +npm install +npm run build +npm run typecheck +npm run test +``` + +Run the MCP inspector: + +```bash +npm run inspector +``` ## Credits -Browser MCP was adapted from the [Playwright MCP server](https://github.com/microsoft/playwright-mcp) in order to automate the user's browser rather than creating new browser instances. This allows using the user's existing browser profile to use logged-in sessions and avoid bot detection mechanisms that commonly block automated browser use. +Browser MCP was created by the [BrowserMCP](https://github.com/BrowserMCP) project and adapted from the [Playwright MCP server](https://github.com/microsoft/playwright-mcp) to automate the user's browser rather than spawning new browser instances. + +This fork is maintained independently. Upstream website and docs remain at [browsermcp.io](https://browsermcp.io) and [docs.browsermcp.io](https://docs.browsermcp.io) for reference. + +## License + +Apache-2.0. See [LICENSE](./LICENSE). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2492fd84 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3756 @@ +{ + "name": "@browsermcp/mcp", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@browsermcp/mcp", + "version": "0.2.0", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "commander": "^15.0.0", + "ws": "^8.18.1", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.2" + }, + "bin": { + "mcp-server-browsermcp": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "@types/ws": "^8.18.1", + "shx": "^0.4.0", + "tsup": "^8.5.0", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/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/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/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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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-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.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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/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.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "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.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/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/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "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.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "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-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/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.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/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/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/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/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "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/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "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/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "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/shelljs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "execa": "^1.0.0", + "fast-glob": "^3.3.2", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/shx": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.8", + "shelljs": "^0.9.2" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/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/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "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/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "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.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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/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/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/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "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 index ac22b68e..8acb2965 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,45 @@ { "name": "@browsermcp/mcp", - "version": "0.1.3", + "version": "0.2.1", "description": "MCP server for browser automation using Browser MCP", "author": "Browser MCP", "homepage": "https://browsermcp.io", "bugs": "https://github.com/browsermcp/mcp/issues", + "license": "Apache-2.0", "type": "module", + "engines": { + "node": ">=18" + }, "bin": { - "mcp-server-browsermcp": "dist/index.js" + "mcp-server-browsermcp": "dist/index.js", + "browsermcp": "dist/index.js", + "browsermcp-ws-daemon": "dist/ws-daemon.js" }, "files": [ "dist" ], "scripts": { "typecheck": "tsc --noEmit", - "build": "tsup src/index.ts --format esm && shx chmod +x dist/*.js", + "build": "tsup && shx chmod +x dist/*.js", + "test": "npm run build && node --import tsx --test test/**/*.test.ts", + "verify": "npm run typecheck && npm run test", "prepare": "npm run build", - "watch": "tsup src/index.ts --format esm --watch ", - "inspector": "CLIENT_PORT=9001 SERVER_PORT=9002 pnpx @modelcontextprotocol/inspector node dist/index.js" + "watch": "tsup --watch", + "inspector": "CLIENT_PORT=9001 SERVER_PORT=9002 npx @modelcontextprotocol/inspector node dist/index.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.8.0", - "commander": "^13.1.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "commander": "^15.0.0", "ws": "^8.18.1", - "zod": "^3.24.2", - "zod-to-json-schema": "^3.24.3" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.2" }, "devDependencies": { - "@r2r/messaging": "workspace:*", - "@repo/config": "workspace:*", - "@repo/messaging": "workspace:*", - "@repo/types": "workspace:*", - "@repo/utils": "workspace:*", - "@types/ws": "^8.18.0", - "shx": "^0.3.4", - "tsup": "^8.4.0", - "typescript": "^5.6.2" + "@types/node": "^22.15.0", + "@types/ws": "^8.18.1", + "shx": "^0.4.0", + "tsup": "^8.5.0", + "tsx": "^4.20.3", + "typescript": "^5.8.3" } } diff --git a/scripts/start-mcp.sh b/scripts/start-mcp.sh new file mode 100755 index 00000000..721435b3 --- /dev/null +++ b/scripts/start-mcp.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DAEMON="$ROOT/dist/ws-daemon.js" +MCP="$ROOT/dist/index.js" + +if ! lsof -iTCP:9010 -sTCP:LISTEN -t >/dev/null 2>&1; then + nohup node "$DAEMON" >>/tmp/browsermcp-ws-daemon.log 2>&1 & + for _ in $(seq 1 40); do + lsof -iTCP:9010 -sTCP:LISTEN -t >/dev/null 2>&1 && break + sleep 0.25 + done +fi + +exec node "$MCP" diff --git a/src/bridge/client.ts b/src/bridge/client.ts new file mode 100644 index 00000000..3e0de161 --- /dev/null +++ b/src/bridge/client.ts @@ -0,0 +1,207 @@ +import net from "node:net"; + +import { mcpConfig } from "@/config/mcp.config"; +import type { SocketMessageRequest } from "@/messaging/ws/types"; +import { readBridgeToken } from "@/security/token"; +import { generateRequestId } from "@/utils/id"; + +import { + encodeBridgeMessage, + parseBridgeEnvelope, + parseBridgeMessages, + type BridgeForwardResponse, +} from "./protocol"; + +type PendingRequest = { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + timeoutId?: ReturnType; +}; + +export class BridgeClient { + private socket: net.Socket; + private buffer = ""; + private pending = new Map(); + private closed = false; + + private constructor(socket: net.Socket) { + this.socket = socket; + socket.setEncoding("utf8"); + socket.on("close", () => this.rejectAll(new Error(mcpConfig.errors.bridgeDisconnected))); + socket.on("error", (error) => + this.rejectAll(error instanceof Error ? error : new Error(String(error))), + ); + } + + static async connect( + port: number = mcpConfig.bridgeControlPort, + timeoutMs: number = mcpConfig.bridgeConnectTimeoutMs, + ): Promise { + const socket = await new Promise((resolve, reject) => { + const connection = net.createConnection({ host: "127.0.0.1", port }); + const timeoutId = setTimeout(() => { + connection.destroy(); + reject(new Error(`Browser MCP bridge not reachable on port ${port}`)); + }, timeoutMs); + + connection.once("connect", () => { + clearTimeout(timeoutId); + resolve(connection); + }); + connection.once("error", (error) => { + clearTimeout(timeoutId); + reject(error); + }); + }); + + const client = new BridgeClient(socket); + await client.authenticate(); + client.attachDataListener(); + return client; + } + + get connected(): boolean { + return !this.closed && !this.socket.destroyed; + } + + async sendSocketMessage( + type: string, + payload: TPayload, + options: { timeoutMs?: number } = { timeoutMs: mcpConfig.defaultRequestTimeoutMs }, + ): Promise { + if (!this.connected) { + throw new Error(mcpConfig.errors.bridgeDisconnected); + } + if (this.pending.size >= mcpConfig.maxPendingBridgeRequests) { + throw new Error(mcpConfig.errors.tooManyPendingRequests); + } + + const id = generateRequestId(); + const message: SocketMessageRequest = { id, type, payload }; + const { timeoutMs } = options; + + return new Promise((resolve, reject) => { + const timeoutId = + timeoutMs && timeoutMs > 0 + ? setTimeout(() => { + this.pending.delete(id); + reject(new Error(`Bridge response timeout after ${timeoutMs}ms`)); + }, timeoutMs) + : undefined; + + this.pending.set(id, { + resolve: (value) => resolve(value as TResult), + reject, + timeoutId, + }); + + this.socket.write(encodeBridgeMessage({ kind: "request", message })); + }); + } + + private attachDataListener() { + this.socket.on("data", (chunk: string | Buffer) => + this.onData(typeof chunk === "string" ? chunk : chunk.toString("utf8")), + ); + } + + private async authenticate(): Promise { + const token = await readBridgeToken(); + this.socket.write(encodeBridgeMessage({ kind: "auth", token })); + + await new Promise((resolve, reject) => { + const authTimeout = setTimeout(() => { + cleanup(); + reject(new Error(mcpConfig.errors.bridgeAuthTimeout)); + }, mcpConfig.bridgeAuthTimeoutMs); + + const onData = (chunk: string | Buffer) => { + this.buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8"); + const { messages, remainder, overflow } = parseBridgeMessages(this.buffer); + this.buffer = remainder; + + if (overflow) { + cleanup(); + reject(new Error(mcpConfig.errors.bridgeAuthFailed)); + return; + } + + for (const raw of messages) { + const parsed = parseBridgeEnvelope(raw); + if (!parsed || parsed.kind !== "auth") { + continue; + } + cleanup(); + if (parsed.ok) { + resolve(); + } else { + reject(new Error(parsed.error ?? mcpConfig.errors.bridgeAuthFailed)); + } + return; + } + }; + + const cleanup = () => { + clearTimeout(authTimeout); + this.socket.off("data", onData); + }; + + this.socket.on("data", onData); + }); + } + + private onData(chunk: string) { + this.buffer += chunk; + const { messages, remainder, overflow } = parseBridgeMessages(this.buffer); + this.buffer = remainder; + + if (overflow) { + this.buffer = ""; + this.rejectAll(new Error("Bridge message buffer overflow")); + this.socket.destroy(); + return; + } + + for (const raw of messages) { + const parsed = parseBridgeEnvelope(raw); + if (!parsed || parsed.kind !== "response" || !parsed.payload) { + continue; + } + const { requestId, result, error } = parsed.payload; + const pending = this.pending.get(requestId); + if (!pending) { + continue; + } + this.pending.delete(requestId); + if (pending.timeoutId) { + clearTimeout(pending.timeoutId); + } + if (error) { + pending.reject(new Error(error)); + } else { + pending.resolve(result); + } + } + } + + private rejectAll(error: Error) { + this.closed = true; + for (const pending of this.pending.values()) { + if (pending.timeoutId) { + clearTimeout(pending.timeoutId); + } + pending.reject(error); + } + this.pending.clear(); + } + + async close() { + if (this.closed) { + return; + } + this.closed = true; + this.socket.removeAllListeners(); + this.socket.destroy(); + this.rejectAll(new Error("Bridge client closed")); + } +} diff --git a/src/bridge/daemon.ts b/src/bridge/daemon.ts new file mode 100644 index 00000000..cf70fbb6 --- /dev/null +++ b/src/bridge/daemon.ts @@ -0,0 +1,224 @@ +import net from "node:net"; + +import { WebSocket, WebSocketServer } from "ws"; + +import { mcpConfig } from "@/config/mcp.config"; +import { MESSAGE_RESPONSE_TYPE } from "@/messaging/ws/types"; +import type { SocketMessageRequest, SocketMessageResponse } from "@/messaging/ws/types"; +import { ensureBridgeToken, tokensMatch } from "@/security/token"; +import { assertAllowedPort } from "@/security/validation"; + +import { + encodeBridgeMessage, + parseBridgeEnvelope, + parseBridgeMessages, + type BridgeForwardResponse, +} from "./protocol"; + +export async function startBridgeDaemon( + wsPort: number = mcpConfig.defaultWsPort, + controlPort: number = mcpConfig.bridgeControlPort, +): Promise<{ close: () => Promise }> { + assertAllowedPort(wsPort); + assertAllowedPort(controlPort); + + const bridgeToken = await ensureBridgeToken(); + let extensionSocket: WebSocket | undefined; + const controlSockets = new Set(); + const requestControlSocket = new Map(); + const controlBuffers = new Map(); + const keepaliveTimers = new Map>(); + + const wss = new WebSocketServer({ port: wsPort, host: "127.0.0.1" }); + await new Promise((resolve, reject) => { + wss.once("listening", () => resolve()); + wss.once("error", reject); + }); + + const controlServer = net.createServer((socket) => { + controlSockets.add(socket); + controlBuffers.set(socket, ""); + let authenticated = false; + + const authTimeout = setTimeout(() => { + socket.destroy(); + }, mcpConfig.bridgeAuthTimeoutMs); + + const rejectAuth = (error: string) => { + clearTimeout(authTimeout); + socket.write( + encodeBridgeMessage({ + kind: "auth", + ok: false, + error, + }), + ); + socket.destroy(); + }; + + socket.setEncoding("utf8"); + socket.on("data", (chunk) => { + let controlBuffer = controlBuffers.get(socket) ?? ""; + controlBuffer += chunk.toString(); + const { messages, remainder, overflow } = parseBridgeMessages(controlBuffer); + controlBuffers.set(socket, remainder); + + if (overflow) { + controlBuffers.set(socket, ""); + socket.destroy(); + return; + } + + for (const raw of messages) { + const parsed = parseBridgeEnvelope(raw); + if (!parsed || parsed.kind !== "auth" && parsed.kind !== "request" && parsed.kind !== "response") { + continue; + } + + if (!authenticated) { + if (parsed.kind !== "auth" || !parsed.token) { + rejectAuth(mcpConfig.errors.bridgeAuthFailed); + return; + } + if (!tokensMatch(bridgeToken, parsed.token)) { + rejectAuth(mcpConfig.errors.bridgeAuthFailed); + return; + } + authenticated = true; + clearTimeout(authTimeout); + socket.write(encodeBridgeMessage({ kind: "auth", ok: true })); + continue; + } + + if (parsed.kind !== "request" || !parsed.message) { + continue; + } + + if (!extensionSocket || extensionSocket.readyState !== WebSocket.OPEN) { + sendBridgeResponse(socket, { + requestId: parsed.message.id, + error: mcpConfig.errors.noConnectedTab, + }); + continue; + } + requestControlSocket.set(parsed.message.id, socket); + extensionSocket.send(JSON.stringify(parsed.message)); + } + }); + + socket.on("close", () => { + clearTimeout(authTimeout); + controlSockets.delete(socket); + controlBuffers.delete(socket); + for (const [requestId, owner] of requestControlSocket.entries()) { + if (owner === socket) { + requestControlSocket.delete(requestId); + } + } + }); + }); + + await new Promise((resolve, reject) => { + controlServer.once("listening", () => resolve()); + controlServer.once("error", reject); + controlServer.listen(controlPort, "127.0.0.1"); + }); + + wss.on("connection", (websocket) => { + if (extensionSocket && extensionSocket !== websocket) { + const previousTimer = keepaliveTimers.get(extensionSocket); + if (previousTimer) { + clearInterval(previousTimer); + keepaliveTimers.delete(extensionSocket); + } + extensionSocket.removeAllListeners(); + extensionSocket.terminate(); + } + + extensionSocket = websocket; + + websocket.on("message", (raw) => { + let message: SocketMessageResponse; + try { + message = JSON.parse(raw.toString()) as SocketMessageResponse; + } catch { + return; + } + if (message.type !== MESSAGE_RESPONSE_TYPE) { + return; + } + const requestId = message.payload.requestId; + const controlSocket = requestControlSocket.get(requestId); + if (!controlSocket || controlSocket.destroyed) { + return; + } + requestControlSocket.delete(requestId); + sendBridgeResponse(controlSocket, message.payload); + }); + + websocket.on("close", () => { + const timer = keepaliveTimers.get(websocket); + if (timer) { + clearInterval(timer); + keepaliveTimers.delete(websocket); + } + if (extensionSocket === websocket) { + extensionSocket = undefined; + } + }); + + websocket.on("pong", () => { + websocket.isAlive = true; + }); + + websocket.isAlive = true; + const timer = setInterval(() => { + if (websocket.readyState !== WebSocket.OPEN) { + clearInterval(timer); + keepaliveTimers.delete(websocket); + return; + } + if (websocket.isAlive === false) { + clearInterval(timer); + keepaliveTimers.delete(websocket); + websocket.terminate(); + return; + } + websocket.isAlive = false; + websocket.ping(); + }, mcpConfig.keepaliveMs); + keepaliveTimers.set(websocket, timer); + }); + + return { + close: async () => { + for (const timer of keepaliveTimers.values()) { + clearInterval(timer); + } + keepaliveTimers.clear(); + for (const socket of controlSockets) { + socket.destroy(); + } + controlSockets.clear(); + requestControlSocket.clear(); + controlBuffers.clear(); + await new Promise((resolve) => controlServer.close(() => resolve())); + await new Promise((resolve) => wss.close(() => resolve())); + }, + }; +} + +function sendBridgeResponse(socket: net.Socket, payload: BridgeForwardResponse) { + socket.write( + encodeBridgeMessage({ + kind: "response", + payload, + }), + ); +} + +declare module "ws" { + interface WebSocket { + isAlive?: boolean; + } +} diff --git a/src/bridge/ensure.ts b/src/bridge/ensure.ts new file mode 100644 index 00000000..c9307112 --- /dev/null +++ b/src/bridge/ensure.ts @@ -0,0 +1,68 @@ +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +import { mcpConfig } from "@/config/mcp.config"; +import { isPortInUse, killProcessOnPort } from "@/utils/port"; + +import { BridgeClient } from "./client"; + +async function canConnectBridge(): Promise { + try { + const client = await BridgeClient.connect(mcpConfig.bridgeControlPort, 1_000); + await client.close(); + return true; + } catch { + return false; + } +} + +async function restartStaleBridge(): Promise { + if (await isPortInUse(mcpConfig.bridgeControlPort)) { + killProcessOnPort(mcpConfig.bridgeControlPort); + } + if (await isPortInUse(mcpConfig.defaultWsPort)) { + killProcessOnPort(mcpConfig.defaultWsPort); + } + await new Promise((resolve) => setTimeout(resolve, 300)); +} + +export async function ensureBridgeDaemon(): Promise { + if (await canConnectBridge()) { + return; + } + + if ( + (await isPortInUse(mcpConfig.bridgeControlPort)) || + (await isPortInUse(mcpConfig.defaultWsPort)) + ) { + await restartStaleBridge(); + } + + const daemonEntry = fileURLToPath(new URL("../ws-daemon.js", import.meta.url)); + const child = spawn(process.execPath, [daemonEntry], { + detached: true, + stdio: "ignore", + env: { + ...process.env, + BROWSERMCP_DAEMON: "1", + }, + }); + child.unref(); + + for (let attempt = 0; attempt < 30; attempt++) { + if (await canConnectBridge()) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + if (await isPortInUse(mcpConfig.defaultWsPort)) { + throw new Error( + `Browser MCP WebSocket port ${mcpConfig.defaultWsPort} is in use but bridge control port ${mcpConfig.bridgeControlPort} is unavailable. Stop stale browsermcp processes or run: node ${daemonEntry}`, + ); + } + + throw new Error( + `Failed to start Browser MCP bridge daemon on port ${mcpConfig.bridgeControlPort}`, + ); +} diff --git a/src/bridge/protocol.ts b/src/bridge/protocol.ts new file mode 100644 index 00000000..08e179ee --- /dev/null +++ b/src/bridge/protocol.ts @@ -0,0 +1,48 @@ +import { mcpConfig } from "@/config/mcp.config"; +import type { SocketMessageRequest, SocketMessageResponse } from "@/messaging/ws/types"; + +export type BridgeForwardRequest = SocketMessageRequest; + +export type BridgeForwardResponse = SocketMessageResponse["payload"]; + +export type BridgeAuthMessage = { + kind: "auth"; + token?: string; + ok?: boolean; + error?: string; +}; + +export type BridgeEnvelope = + | BridgeAuthMessage + | { kind: "request"; message: BridgeForwardRequest } + | { kind: "response"; payload: BridgeForwardResponse }; + +export function encodeBridgeMessage(message: unknown): string { + return `${JSON.stringify(message)}\n`; +} + +export function parseBridgeMessages( + buffer: string, + maxBytes: number = mcpConfig.maxBridgeBufferBytes, +): { + messages: string[]; + remainder: string; + overflow: boolean; +} { + if (Buffer.byteLength(buffer, "utf8") > maxBytes) { + return { messages: [], remainder: "", overflow: true }; + } + + const parts = buffer.split("\n"); + const remainder = parts.pop() ?? ""; + const messages = parts.filter((line) => line.trim().length > 0); + return { messages, remainder, overflow: false }; +} + +export function parseBridgeEnvelope(raw: string): BridgeEnvelope | undefined { + try { + return JSON.parse(raw) as BridgeEnvelope; + } catch { + return undefined; + } +} diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 00000000..da1e2dfd --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,9 @@ +export const appConfig = { + name: "Browser MCP", + tagline: "Automate your browser with AI", + description: + "Browser MCP connects AI applications to your browser so you can automate tasks using AI. Supported by Claude, Cursor, VS Code, Windsurf, and more.", + email: { + defaultFrom: "support@mail.browsermcp.io", + }, +} as const; diff --git a/src/config/mcp.config.ts b/src/config/mcp.config.ts new file mode 100644 index 00000000..8db19a08 --- /dev/null +++ b/src/config/mcp.config.ts @@ -0,0 +1,29 @@ +export const mcpConfig = { + defaultWsPort: 9009, + bridgeControlPort: 9010, + keepaliveMs: 25_000, + bridgeAuthTimeoutMs: 5_000, + bridgeConnectTimeoutMs: 5_000, + defaultRequestTimeoutMs: 30_000, + maxBridgeBufferBytes: 10 * 1024 * 1024, + maxPendingBridgeRequests: 100, + limits: { + maxUrlLength: 8_192, + maxWaitSeconds: 120, + maxTextLength: 10_000, + maxKeyLength: 128, + maxRefLength: 256, + maxElementDescriptionLength: 512, + maxSelectValues: 50, + maxSelectValueLength: 256, + maxConsoleLogLines: 500, + }, + errors: { + noConnectedTab: "No tab is connected", + bridgeDisconnected: + "Browser MCP bridge disconnected. Re-open the Browser MCP extension and click Connect.", + bridgeAuthFailed: "Bridge authentication failed", + bridgeAuthTimeout: "Bridge authentication timed out", + tooManyPendingRequests: "Too many pending bridge requests", + }, +} as const; diff --git a/src/context.ts b/src/context.ts index 1e2fa3c0..a029e57d 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,14 +1,20 @@ -import { createSocketMessageSender } from "@r2r/messaging/ws/sender"; import { WebSocket } from "ws"; -import { mcpConfig } from "@repo/config/mcp.config"; -import { MessagePayload, MessageType } from "@repo/messaging/types"; -import { SocketMessageMap } from "@repo/types/messages/ws"; +import { BridgeClient } from "@/bridge/client"; +import { mcpConfig } from "@/config/mcp.config"; +import type { + MessagePayload, + MessageResult, + MessageType, +} from "@/messaging/types"; +import { createSocketMessageSender } from "@/messaging/ws/sender"; +import type { SocketMessageMap } from "@/types/messages/ws"; const noConnectionMessage = `No connection to browser extension. In order to proceed, you must first connect a tab by clicking the Browser MCP extension icon in the browser toolbar and clicking the 'Connect' button.`; export class Context { private _ws: WebSocket | undefined; + private _bridge: BridgeClient | undefined; get ws(): WebSocket { if (!this._ws) { @@ -21,32 +27,77 @@ export class Context { this._ws = ws; } + set bridge(bridge: BridgeClient) { + this._bridge = bridge; + } + hasWs(): boolean { return !!this._ws; } + usesBridge(): boolean { + return !!this._bridge; + } + + clearWs(): void { + this._ws = undefined; + } + async sendSocketMessage>( type: T, payload: MessagePayload, - options: { timeoutMs?: number } = { timeoutMs: 30000 }, - ) { - const { sendSocketMessage } = createSocketMessageSender( - this.ws, - ); + options: { timeoutMs?: number } = { timeoutMs: mcpConfig.defaultRequestTimeoutMs }, + ): Promise> { try { - return await sendSocketMessage(type, payload, options); + if (this._bridge) { + if (!this._bridge.connected) { + await this._bridge.close().catch(() => undefined); + this._bridge = await BridgeClient.connect(); + } + return (await this._bridge.sendSocketMessage( + type, + payload, + options, + )) as MessageResult; + } + + const { sendSocketMessage } = createSocketMessageSender(this.ws); + return await sendSocketMessage< + MessagePayload, + MessageResult + >(type, payload, options); } catch (e) { - if (e instanceof Error && e.message === mcpConfig.errors.noConnectedTab) { - throw new Error(noConnectionMessage); + if (e instanceof Error) { + if (e.message === mcpConfig.errors.noConnectedTab) { + throw new Error(noConnectionMessage); + } + if (e.message === mcpConfig.errors.bridgeDisconnected) { + throw new Error(noConnectionMessage); + } + if (e.message === "WebSocket is not open") { + throw new Error(noConnectionMessage); + } } throw e; } } async close() { + if (this._bridge) { + await this._bridge.close(); + this._bridge = undefined; + } if (!this._ws) { return; } - await this._ws.close(); + const ws = this._ws; + this._ws = undefined; + ws.removeAllListeners(); + if ( + ws.readyState === WebSocket.OPEN || + ws.readyState === WebSocket.CONNECTING + ) { + ws.close(); + } } } diff --git a/src/index.ts b/src/index.ts index 2278a755..63eb728a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,8 @@ -#!/usr/bin/env node import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { program } from "commander"; -import { appConfig } from "@repo/config/app.config"; +import { appConfig } from "@/config/app.config"; import type { Resource } from "@/resources/resource"; import { createServerWithTools } from "@/server"; @@ -12,6 +11,9 @@ import * as custom from "@/tools/custom"; import * as snapshot from "@/tools/snapshot"; import type { Tool } from "@/tools/tool"; +import { pairMcpClients } from "@/pairing/pair"; +import type { McpClientId } from "@/pairing/types"; + import packageJSON from "../package.json"; function setupExitWatchdog(server: Server) { @@ -50,17 +52,56 @@ async function createServer(): Promise { }); } +const MCP_CLIENT_IDS = ["cursor", "claude", "windsurf", "vscode"] as const; + +function parseClientIds(value: string): McpClientId[] { + return value + .split(",") + .map((part) => part.trim()) + .filter((part): part is McpClientId => + (MCP_CLIENT_IDS as readonly string[]).includes(part), + ); +} + /** * Note: Tools must be defined *before* calling `createServer` because only declarations are hoisted, not the initializations */ program .version("Version " + packageJSON.version) .name(packageJSON.name) - .action(async () => { - const server = await createServer(); - setupExitWatchdog(server); + .command("pair") + .alias("setup") + .description("Install Browser MCP in your editor with one-click pairing links") + .option("--local", "Use the local build instead of npx @browsermcp/mcp") + .option("--clients ", "Comma-separated clients: cursor,claude,windsurf,vscode") + .option("--open [client]", "Open a one-click install link (cursor, vscode, or all)") + .option("--skip-daemon", "Do not start the bridge daemon") + .action(async (options: { + local?: boolean; + clients?: string; + open?: boolean | string; + skipDaemon?: boolean; + }) => { + let open: McpClientId | "all" | undefined; + if (options.open === true) { + open = "cursor"; + } else if (typeof options.open === "string") { + open = options.open === "all" ? "all" : (options.open as McpClientId); + } - const transport = new StdioServerTransport(); - await server.connect(transport); + await pairMcpClients({ + local: options.local, + clients: options.clients ? parseClientIds(options.clients) : undefined, + open, + skipDaemon: options.skipDaemon, + }); }); + +program.action(async () => { + const server = await createServer(); + setupExitWatchdog(server); + + const transport = new StdioServerTransport(); + await server.connect(transport); +}); program.parse(process.argv); diff --git a/src/messaging/types.ts b/src/messaging/types.ts new file mode 100644 index 00000000..faee22b9 --- /dev/null +++ b/src/messaging/types.ts @@ -0,0 +1,17 @@ +export type SocketMessageDefinition = { + payload: unknown; + result: unknown; +}; + +export type MessageType> = + keyof M & string; + +export type MessagePayload< + M extends Record, + T extends MessageType, +> = M[T]["payload"]; + +export type MessageResult< + M extends Record, + T extends MessageType, +> = M[T]["result"]; diff --git a/src/messaging/ws/sender.ts b/src/messaging/ws/sender.ts new file mode 100644 index 00000000..a76f3926 --- /dev/null +++ b/src/messaging/ws/sender.ts @@ -0,0 +1,91 @@ +import { WebSocket } from "ws"; + +import { mcpConfig } from "@/config/mcp.config"; +import { generateRequestId } from "@/utils/id"; + +import { + MESSAGE_RESPONSE_TYPE, + type SocketMessageRequest, + type SocketMessageResponse, +} from "./types"; + +export function createSocketMessageSender(ws: WebSocket) { + async function sendSocketMessage( + type: string, + payload: TPayload, + options: { timeoutMs?: number } = { timeoutMs: mcpConfig.defaultRequestTimeoutMs }, + ): Promise { + const { timeoutMs } = options; + const id = generateRequestId(); + const message: SocketMessageRequest = { id, type, payload }; + + return new Promise((resolve, reject) => { + const cleanup = () => { + removeSocketMessageResponseListener(); + ws.removeEventListener("error", errorHandler); + ws.removeEventListener("close", cleanup); + clearTimeout(timeoutId); + }; + + let timeoutId: ReturnType | undefined; + if (timeoutMs) { + timeoutId = setTimeout(() => { + cleanup(); + reject(new Error(`WebSocket response timeout after ${timeoutMs}ms`)); + }, timeoutMs); + } + + const removeSocketMessageResponseListener = addSocketMessageResponseListener( + ws, + (responseMessage: SocketMessageResponse) => { + const { payload: responsePayload } = responseMessage; + if (responsePayload.requestId !== id) { + return; + } + const { result, error } = responsePayload; + if (error) { + reject(new Error(error)); + } else { + resolve(result as TResult); + } + cleanup(); + }, + ); + + const errorHandler = () => { + cleanup(); + reject(new Error("WebSocket error occurred")); + }; + + ws.addEventListener("error", errorHandler); + ws.addEventListener("close", cleanup); + + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(message)); + } else { + cleanup(); + reject(new Error("WebSocket is not open")); + } + }); + } + + return { sendSocketMessage }; +} + +function addSocketMessageResponseListener( + ws: WebSocket, + typeListener: (message: SocketMessageResponse) => void | Promise, +) { + const listener = async (rawData: WebSocket.RawData) => { + const message = JSON.parse( + rawData.toString(), + ) as SocketMessageResponse; + if (message.type !== MESSAGE_RESPONSE_TYPE) { + return; + } + await typeListener(message); + }; + ws.on("message", listener); + return () => ws.off("message", listener); +} + diff --git a/src/messaging/ws/types.ts b/src/messaging/ws/types.ts new file mode 100644 index 00000000..09139883 --- /dev/null +++ b/src/messaging/ws/types.ts @@ -0,0 +1,16 @@ +export const MESSAGE_RESPONSE_TYPE = "messageResponse" as const; + +export type SocketMessageResponse = { + type: typeof MESSAGE_RESPONSE_TYPE; + payload: { + requestId: string; + result?: TResult; + error?: string; + }; +}; + +export type SocketMessageRequest = { + id: string; + type: string; + payload: TPayload; +}; diff --git a/src/pairing/clients.ts b/src/pairing/clients.ts new file mode 100644 index 00000000..6edef949 --- /dev/null +++ b/src/pairing/clients.ts @@ -0,0 +1,55 @@ +import os from "node:os"; +import path from "node:path"; + +import type { McpClientId, McpClientTarget } from "./types"; + +function homePath(...segments: string[]): string { + return path.join(os.homedir(), ...segments); +} + +const ALL_CLIENTS: McpClientTarget[] = [ + { + id: "cursor", + label: "Cursor", + configPath: homePath(".cursor", "mcp.json"), + }, + { + id: "claude", + label: "Claude Desktop", + configPath: + process.platform === "darwin" + ? homePath("Library", "Application Support", "Claude", "claude_desktop_config.json") + : process.platform === "win32" + ? homePath("AppData", "Roaming", "Claude", "claude_desktop_config.json") + : homePath(".config", "Claude", "claude_desktop_config.json"), + }, + { + id: "windsurf", + label: "Windsurf", + configPath: homePath(".codeium", "windsurf", "mcp_config.json"), + }, + { + id: "vscode", + label: "VS Code", + configPath: homePath(".vscode", "mcp.json"), + }, +]; + +export function listMcpClients(): McpClientTarget[] { + return ALL_CLIENTS; +} + +export function getMcpClient(id: McpClientId): McpClientTarget | undefined { + return ALL_CLIENTS.find((client) => client.id === id); +} + +export function resolveClientTargets(ids?: McpClientId[]): McpClientTarget[] { + if (!ids?.length) { + return ALL_CLIENTS; + } + + return ids.flatMap((id) => { + const client = getMcpClient(id); + return client ? [client] : []; + }); +} diff --git a/src/pairing/install.ts b/src/pairing/install.ts new file mode 100644 index 00000000..feb06a58 --- /dev/null +++ b/src/pairing/install.ts @@ -0,0 +1,51 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +import type { McpClientTarget, McpConfigFile, McpServerEntry } from "./types"; +import { SERVER_NAME } from "./types"; + +function readConfigFile(configPath: string): McpConfigFile { + if (!existsSync(configPath)) { + return { mcpServers: {} }; + } + + const raw = readFileSync(configPath, "utf8").trim(); + if (!raw) { + return { mcpServers: {} }; + } + + const parsed = JSON.parse(raw) as Partial; + return { + mcpServers: parsed.mcpServers ?? {}, + }; +} + +function writeConfigFile(configPath: string, config: McpConfigFile): void { + mkdirSync(path.dirname(configPath), { recursive: true }); + writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); +} + +function entriesMatch(existing: McpServerEntry, next: McpServerEntry): boolean { + return ( + existing.command === next.command && + existing.args.length === next.args.length && + existing.args.every((arg, index) => arg === next.args[index]) && + JSON.stringify(existing.env ?? {}) === JSON.stringify(next.env ?? {}) + ); +} + +export function installServerConfig( + client: McpClientTarget, + serverConfig: McpServerEntry, +): { updated: boolean } { + const config = readConfigFile(client.configPath); + const existing = config.mcpServers[SERVER_NAME]; + + if (existing && entriesMatch(existing, serverConfig)) { + return { updated: false }; + } + + config.mcpServers[SERVER_NAME] = serverConfig; + writeConfigFile(client.configPath, config); + return { updated: true }; +} diff --git a/src/pairing/links.ts b/src/pairing/links.ts new file mode 100644 index 00000000..23d6429a --- /dev/null +++ b/src/pairing/links.ts @@ -0,0 +1,31 @@ +import type { McpServerEntry } from "./types"; +import { SERVER_NAME } from "./types"; + +function encodeBase64Url(value: string): string { + return Buffer.from(value, "utf8").toString("base64url"); +} + +export function createCursorInstallLink(serverConfig: McpServerEntry): string { + const config = encodeBase64Url(JSON.stringify(serverConfig)); + const name = encodeURIComponent(SERVER_NAME); + return `cursor://anysphere.cursor-deeplink/mcp/install?name=${name}&config=${config}`; +} + +export function createVsCodeInstallLink(serverConfig: McpServerEntry): string { + const payload = JSON.stringify({ + name: SERVER_NAME, + type: "stdio", + ...serverConfig, + }); + return `vscode://mcp/install?${encodeURIComponent(payload)}`; +} + +export function createInstallLinks(serverConfig: McpServerEntry): { + cursor: string; + vscode: string; +} { + return { + cursor: createCursorInstallLink(serverConfig), + vscode: createVsCodeInstallLink(serverConfig), + }; +} diff --git a/src/pairing/open-link.ts b/src/pairing/open-link.ts new file mode 100644 index 00000000..72c231e3 --- /dev/null +++ b/src/pairing/open-link.ts @@ -0,0 +1,22 @@ +import { spawn } from "node:child_process"; + +export async function openInstallLink(url: string): Promise { + const command = + process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; + + await new Promise((resolve, reject) => { + const child = + process.platform === "win32" + ? spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", shell: false }) + : spawn(command, [url], { stdio: "ignore" }); + + child.once("error", reject); + child.once("exit", (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`Failed to open install link (exit ${code ?? "unknown"})`)); + }); + }); +} diff --git a/src/pairing/pair.ts b/src/pairing/pair.ts new file mode 100644 index 00000000..5c92be7a --- /dev/null +++ b/src/pairing/pair.ts @@ -0,0 +1,105 @@ +import { ensureBridgeDaemon } from "@/bridge/ensure"; + +import { resolveClientTargets } from "./clients"; +import { installServerConfig } from "./install"; +import { createInstallLinks } from "./links"; +import { openInstallLink } from "./open-link"; +import { resolveServerConfig } from "./server-config"; +import type { McpClientId, PairOptions, PairResult } from "./types"; + +function log(message: string): void { + console.log(message); +} + +function logStep(title: string, body: string): void { + log(`\n${title}`); + log(body); +} + +export async function pairMcpClients(options: PairOptions = {}): Promise { + const serverConfig = resolveServerConfig({ local: options.local }); + const targets = resolveClientTargets(options.clients); + const links = createInstallLinks(serverConfig); + + const installed: PairResult["installed"] = []; + const skipped: PairResult["skipped"] = []; + + for (const client of targets) { + const { updated } = installServerConfig(client, serverConfig); + if (updated) { + installed.push(client); + } else { + skipped.push(client); + } + } + + let daemonStarted = false; + if (!options.skipDaemon) { + try { + await ensureBridgeDaemon(); + daemonStarted = true; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logStep("Bridge daemon", `Could not start automatically: ${message}`); + } + } + + log("Browser MCP pairing complete.\n"); + + if (installed.length) { + logStep( + "Installed MCP config", + installed.map((client) => `- ${client.label}: ${client.configPath}`).join("\n"), + ); + } + + if (skipped.length) { + logStep( + "Already configured or skipped", + skipped.map((client) => `- ${client.label}`).join("\n"), + ); + } + + logStep("One-click install links", [ + `Cursor: ${links.cursor}`, + `VS Code: ${links.vscode}`, + ].join("\n")); + + if (daemonStarted) { + logStep("Bridge daemon", "Running on ports 9009 (extension) and 9010 (MCP)."); + } + + logStep( + "Next steps", + [ + "1. Restart MCP in your editor (Cursor: Settings → MCP → restart browsermcp).", + "2. Open the Browser MCP Chrome extension and click Connect on your tab.", + "3. Or use a one-click link above to install in Cursor / VS Code.", + ].join("\n"), + ); + + if (options.open) { + const clientsToOpen: McpClientId[] = + options.open === "all" ? ["cursor", "vscode"] : [options.open]; + + for (const clientId of clientsToOpen) { + const link = clientId === "vscode" ? links.vscode : links.cursor; + try { + await openInstallLink(link); + log(`\nOpened ${clientId} install link.`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`\nCould not open ${clientId} install link: ${message}`); + log(link); + } + } + } + + return { + serverConfig, + installed, + skipped, + links, + daemonStarted, + }; +} diff --git a/src/pairing/server-config.ts b/src/pairing/server-config.ts new file mode 100644 index 00000000..68019ec9 --- /dev/null +++ b/src/pairing/server-config.ts @@ -0,0 +1,56 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import packageJSON from "../../package.json"; + +import type { McpServerEntry } from "./types"; + +function getPackageRoot(): string { + const entry = fileURLToPath(import.meta.url); + if (entry.includes(`${path.sep}dist${path.sep}`)) { + return path.dirname(path.dirname(entry)); + } + return path.dirname(path.dirname(path.dirname(entry))); +} + +function isLocalDevelopment(): boolean { + const pkgRoot = getPackageRoot(); + const pkgPath = path.join(pkgRoot, "package.json"); + + if (!existsSync(pkgPath)) { + return false; + } + + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string }; + return pkg.name === "@browsermcp/mcp" && existsSync(path.join(pkgRoot, "src")); + } catch { + return false; + } +} + +export function resolveServerEntryPath(): string { + return fileURLToPath(import.meta.url); +} + +export function resolveServerConfig(options: { local?: boolean } = {}): McpServerEntry { + const useLocal = options.local ?? isLocalDevelopment(); + + if (useLocal) { + return { + command: process.execPath, + args: [resolveServerEntryPath()], + }; + } + + return { + command: "npx", + args: ["-y", `@browsermcp/mcp@${packageJSON.version}`], + }; +} + +export function isUsingLocalServerConfig(config: McpServerEntry): boolean { + const entryPath = resolveServerEntryPath(); + return config.command === process.execPath && config.args.includes(entryPath); +} diff --git a/src/pairing/types.ts b/src/pairing/types.ts new file mode 100644 index 00000000..b8f7a74a --- /dev/null +++ b/src/pairing/types.ts @@ -0,0 +1,37 @@ +export const SERVER_NAME = "browsermcp" as const; + +export type McpServerEntry = { + command: string; + args: string[]; + env?: Record; +}; + +export type McpConfigFile = { + mcpServers: Record; +}; + +export type McpClientId = "cursor" | "claude" | "windsurf" | "vscode"; + +export type McpClientTarget = { + id: McpClientId; + label: string; + configPath: string; +}; + +export type PairOptions = { + clients?: McpClientId[]; + local?: boolean; + open?: McpClientId | "all"; + skipDaemon?: boolean; +}; + +export type PairResult = { + serverConfig: McpServerEntry; + installed: McpClientTarget[]; + skipped: McpClientTarget[]; + links: { + cursor?: string; + vscode?: string; + }; + daemonStarted: boolean; +}; diff --git a/src/security/token.ts b/src/security/token.ts new file mode 100644 index 00000000..85dbe361 --- /dev/null +++ b/src/security/token.ts @@ -0,0 +1,76 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import path from "node:path"; +import { timingSafeEqual } from "node:crypto"; + +const TOKEN_FILENAME = "bridge.token"; +const TOKEN_BYTES = 32; + +function tokenDir(): string { + return process.env.BROWSERMCP_TOKEN_DIR ?? path.join(homedir(), ".browsermcp"); +} + +function tokenPath(): string { + return path.join(tokenDir(), TOKEN_FILENAME); +} + +export function getBridgeTokenFromEnv(): string | undefined { + const value = process.env.BROWSERMCP_BRIDGE_TOKEN?.trim(); + return value || undefined; +} + +export async function ensureBridgeToken(): Promise { + const fromEnv = getBridgeTokenFromEnv(); + if (fromEnv) { + return fromEnv; + } + + const file = tokenPath(); + try { + const existing = (await readFile(file, "utf8")).trim(); + if (existing.length >= 16) { + return existing; + } + } catch { + // Generate a new token below. + } + + const token = generateToken(); + await mkdir(tokenDir(), { recursive: true, mode: 0o700 }); + await writeFile(file, `${token}\n`, { encoding: "utf8", mode: 0o600 }); + return token; +} + +export async function readBridgeToken(): Promise { + const fromEnv = getBridgeTokenFromEnv(); + if (fromEnv) { + return fromEnv; + } + + const file = tokenPath(); + const token = (await readFile(file, "utf8")).trim(); + if (!token) { + throw new Error( + "Browser MCP bridge token is missing. Restart the bridge daemon or set BROWSERMCP_BRIDGE_TOKEN.", + ); + } + return token; +} + +export function tokensMatch(expected: string, provided: string): boolean { + const a = Buffer.from(expected); + const b = Buffer.from(provided); + if (a.length !== b.length) { + return false; + } + return timingSafeEqual(a, b); +} + +function generateToken(): string { + if (typeof globalThis.crypto?.getRandomValues === "function") { + const bytes = new Uint8Array(TOKEN_BYTES); + globalThis.crypto.getRandomValues(bytes); + return Buffer.from(bytes).toString("base64url"); + } + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`; +} diff --git a/src/security/validation.ts b/src/security/validation.ts new file mode 100644 index 00000000..6b3412ff --- /dev/null +++ b/src/security/validation.ts @@ -0,0 +1,33 @@ +import { mcpConfig } from "@/config/mcp.config"; + +const BLOCKED_URL_PROTOCOLS = /^(javascript|data|file|vbscript|blob):/i; + +export function assertSafeNavigationUrl(url: string): void { + const trimmed = url.trim(); + if (!trimmed) { + throw new Error("URL is required"); + } + if (trimmed.length > mcpConfig.limits.maxUrlLength) { + throw new Error(`URL exceeds maximum length of ${mcpConfig.limits.maxUrlLength}`); + } + if (BLOCKED_URL_PROTOCOLS.test(trimmed)) { + throw new Error("Blocked URL protocol. Only http and https URLs are allowed."); + } + + let parsed: URL; + try { + parsed = new URL(trimmed); + } catch { + throw new Error("Invalid URL"); + } + + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("Only http and https URLs are allowed."); + } +} + +export function assertAllowedPort(port: number): void { + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + throw new Error(`Invalid port: ${port}`); + } +} diff --git a/src/server.ts b/src/server.ts index 7c118f0e..cb78a08b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,21 +5,30 @@ import { ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; +import { WebSocket } from "ws"; +import { BridgeClient } from "@/bridge/client"; +import { ensureBridgeDaemon } from "@/bridge/ensure"; import { Context } from "@/context"; import type { Resource } from "@/resources/resource"; import type { Tool } from "@/tools/tool"; import { createWebSocketServer } from "@/ws"; +import { mcpConfig } from "@/config/mcp.config"; + type Options = { name: string; version: string; tools: Tool[]; resources: Resource[]; + /** WebSocket listen port (defaults to mcp config). */ + wsPort?: number; + /** Use persistent bridge daemon instead of embedded WebSocket server. */ + useBridge?: boolean; }; export async function createServerWithTools(options: Options): Promise { - const { name, version, tools, resources } = options; + const { name, version, tools, resources, wsPort, useBridge = true } = options; const context = new Context(); const server = new Server( { name, version }, @@ -31,14 +40,20 @@ export async function createServerWithTools(options: Options): Promise { }, ); - const wss = await createWebSocketServer(); - wss.on("connection", (websocket) => { - // Close any existing connections - if (context.hasWs()) { - context.ws.close(); - } - context.ws = websocket; - }); + let wss: Awaited> | undefined; + + if (useBridge) { + await ensureBridgeDaemon(); + context.bridge = await BridgeClient.connect(); + } else { + wss = await createWebSocketServer(wsPort); + wss.on("connection", (websocket) => { + attachExtensionSocket(context, websocket); + }); + } + + const toolByName = new Map(tools.map((tool) => [tool.schema.name, tool])); + const resourceByUri = new Map(resources.map((resource) => [resource.schema.uri, resource])); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: tools.map((tool) => tool.schema) }; @@ -49,7 +64,7 @@ export async function createServerWithTools(options: Options): Promise { }); server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = tools.find((tool) => tool.schema.name === request.params.name); + const tool = toolByName.get(request.params.name); if (!tool) { return { content: [ @@ -71,9 +86,7 @@ export async function createServerWithTools(options: Options): Promise { }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const resource = resources.find( - (resource) => resource.schema.uri === request.params.uri, - ); + const resource = resourceByUri.get(request.params.uri); if (!resource) { return { contents: [] }; } @@ -82,11 +95,54 @@ export async function createServerWithTools(options: Options): Promise { return { contents }; }); + const closeServer = server.close.bind(server); server.close = async () => { - await server.close(); - await wss.close(); + if (wss) { + await new Promise((resolve) => wss!.close(() => resolve())); + } await context.close(); + await closeServer(); }; return server; } + +function attachExtensionSocket(context: Context, websocket: WebSocket) { + if (context.hasWs()) { + const previous = context.ws; + previous.removeAllListeners(); + previous.close(); + } + context.ws = websocket; + + websocket.on("close", () => { + if (context.hasWs() && context.ws === websocket) { + context.clearWs(); + } + }); + + websocket.on("pong", () => { + websocket.isAlive = true; + }); + + websocket.isAlive = true; + const timer = setInterval(() => { + if (websocket.readyState !== WebSocket.OPEN) { + clearInterval(timer); + return; + } + if (websocket.isAlive === false) { + clearInterval(timer); + websocket.terminate(); + return; + } + websocket.isAlive = false; + websocket.ping(); + }, mcpConfig.keepaliveMs); +} + +declare module "ws" { + interface WebSocket { + isAlive?: boolean; + } +} diff --git a/src/tools/common.ts b/src/tools/common.ts index 3dae8341..3930a90d 100644 --- a/src/tools/common.ts +++ b/src/tools/common.ts @@ -6,7 +6,7 @@ import { NavigateTool, PressKeyTool, WaitTool, -} from "@repo/types/mcp/tool"; +} from "@/types/mcp/tool"; import { captureAriaSnapshot } from "@/utils/aria-snapshot"; diff --git a/src/tools/custom.ts b/src/tools/custom.ts index afaffe42..3819b328 100644 --- a/src/tools/custom.ts +++ b/src/tools/custom.ts @@ -1,8 +1,9 @@ import { zodToJsonSchema } from "zod-to-json-schema"; -import { GetConsoleLogsTool, ScreenshotTool } from "@repo/types/mcp/tool"; +import { mcpConfig } from "@/config/mcp.config"; +import { GetConsoleLogsTool, ScreenshotTool } from "@/types/mcp/tool"; -import { Tool } from "./tool"; +import type { Tool } from "./tool"; export const getConsoleLogs: Tool = { schema: { @@ -15,7 +16,8 @@ export const getConsoleLogs: Tool = { "browser_get_console_logs", {}, ); - const text: string = consoleLogs + const text = consoleLogs + .slice(0, mcpConfig.limits.maxConsoleLogLines) .map((log) => JSON.stringify(log)) .join("\n"); return { @@ -31,7 +33,7 @@ export const screenshot: Tool = { inputSchema: zodToJsonSchema(ScreenshotTool.shape.arguments), }, handle: async (context, _params) => { - const screenshot = await context.sendSocketMessage( + const imageData = await context.sendSocketMessage( "browser_screenshot", {}, ); @@ -39,7 +41,7 @@ export const screenshot: Tool = { content: [ { type: "image", - data: screenshot, + data: imageData, mimeType: "image/png", }, ], diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 6598f3b4..f8fb7626 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -1,4 +1,4 @@ -import zodToJsonSchema from "zod-to-json-schema"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { ClickTool, @@ -7,7 +7,7 @@ import { SelectOptionTool, SnapshotTool, TypeTool, -} from "@repo/types/mcp/tool"; +} from "@/types/mcp/tool"; import type { Context } from "@/context"; import { captureAriaSnapshot } from "@/utils/aria-snapshot"; diff --git a/src/types/mcp/tool.ts b/src/types/mcp/tool.ts new file mode 100644 index 00000000..65c55024 --- /dev/null +++ b/src/types/mcp/tool.ts @@ -0,0 +1,180 @@ +import { z } from "zod"; + +import { mcpConfig } from "@/config/mcp.config"; +import { assertSafeNavigationUrl } from "@/security/validation"; + +const { limits } = mcpConfig; + +export type ConsoleLog = { + level?: string; + message?: string; + timestamp?: number; + [key: string]: unknown; +}; + +const ElementSchema = z.object({ + element: z + .string() + .min(1) + .max(limits.maxElementDescriptionLength) + .describe( + "Human-readable element description used to obtain permission to interact with the element", + ), + ref: z + .string() + .min(1) + .max(limits.maxRefLength) + .describe("Exact target element reference from the page snapshot"), +}); + +export const NavigateTool = z.object({ + name: z.literal("browser_navigate"), + description: z.literal("Navigate to a URL"), + arguments: z.object({ + url: z + .string() + .min(1) + .max(limits.maxUrlLength) + .describe("The URL to navigate to") + .superRefine((url, ctx) => { + try { + assertSafeNavigationUrl(url); + } catch (error) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: error instanceof Error ? error.message : "Invalid URL", + }); + } + }), + }), +}); + +export const GoBackTool = z.object({ + name: z.literal("browser_go_back"), + description: z.literal("Go back to the previous page"), + arguments: z.object({}), +}); + +export const GoForwardTool = z.object({ + name: z.literal("browser_go_forward"), + description: z.literal("Go forward to the next page"), + arguments: z.object({}), +}); + +export const WaitTool = z.object({ + name: z.literal("browser_wait"), + description: z.literal("Wait for a specified time in seconds"), + arguments: z.object({ + time: z + .number() + .positive() + .max(limits.maxWaitSeconds) + .describe("The time to wait in seconds"), + }), +}); + +export const PressKeyTool = z.object({ + name: z.literal("browser_press_key"), + description: z.literal("Press a key on the keyboard"), + arguments: z.object({ + key: z + .string() + .min(1) + .max(limits.maxKeyLength) + .describe( + "Name of the key to press or a character to generate, such as `ArrowLeft` or `a`", + ), + }), +}); + +export const SnapshotTool = z.object({ + name: z.literal("browser_snapshot"), + description: z.literal( + "Capture accessibility snapshot of the current page. Use this for getting references to elements to interact with.", + ), + arguments: z.object({}), +}); + +export const ClickTool = z.object({ + name: z.literal("browser_click"), + description: z.literal("Perform click on a web page"), + arguments: ElementSchema, +}); + +export const DragTool = z.object({ + name: z.literal("browser_drag"), + description: z.literal("Perform drag and drop between two elements"), + arguments: z.object({ + startElement: z.string().min(1).max(limits.maxElementDescriptionLength).describe( + "Human-readable source element description used to obtain the permission to interact with the element", + ), + startRef: z.string().min(1).max(limits.maxRefLength).describe( + "Exact source element reference from the page snapshot", + ), + endElement: z.string().min(1).max(limits.maxElementDescriptionLength).describe( + "Human-readable target element description used to obtain the permission to interact with the element", + ), + endRef: z.string().min(1).max(limits.maxRefLength).describe( + "Exact target element reference from the page snapshot", + ), + }), +}); + +export const HoverTool = z.object({ + name: z.literal("browser_hover"), + description: z.literal("Hover over element on page"), + arguments: ElementSchema, +}); + +export const TypeTool = z.object({ + name: z.literal("browser_type"), + description: z.literal("Type text into editable element"), + arguments: ElementSchema.extend({ + text: z.string().max(limits.maxTextLength).describe("Text to type into the element"), + submit: z + .boolean() + .describe("Whether to submit entered text (press Enter after)"), + }), +}); + +export const SelectOptionTool = z.object({ + name: z.literal("browser_select_option"), + description: z.literal("Select an option in a dropdown"), + arguments: ElementSchema.extend({ + values: z + .array(z.string().min(1).max(limits.maxSelectValueLength)) + .min(1) + .max(limits.maxSelectValues) + .describe( + "Array of values to select in the dropdown. This can be a single value or multiple values.", + ), + }), +}); + +export const ScreenshotTool = z.object({ + name: z.literal("browser_screenshot"), + description: z.literal("Take a screenshot of the current page"), + arguments: z.object({}), +}); + +export const GetConsoleLogsTool = z.object({ + name: z.literal("browser_get_console_logs"), + description: z.literal("Get the console logs from the browser"), + arguments: z.object({}), +}); + +export const MCPTool = z.discriminatedUnion("name", [ + NavigateTool, + GoBackTool, + GoForwardTool, + WaitTool, + PressKeyTool, + SnapshotTool, + ClickTool, + DragTool, + HoverTool, + TypeTool, + SelectOptionTool, + ScreenshotTool, + GetConsoleLogsTool, +]); diff --git a/src/types/messages/ws.ts b/src/types/messages/ws.ts new file mode 100644 index 00000000..5dd9f560 --- /dev/null +++ b/src/types/messages/ws.ts @@ -0,0 +1,48 @@ +type EmptyPayload = Record; + +type ConsoleLog = { + level?: string; + message?: string; + timestamp?: number; + [key: string]: unknown; +}; + +type ElementInteractionPayload = { + element: string; + ref: string; +}; + +type DragPayload = { + startElement: string; + startRef: string; + endElement: string; + endRef: string; +}; + +type TypePayload = ElementInteractionPayload & { + text: string; + submit: boolean; +}; + +type SelectOptionPayload = ElementInteractionPayload & { + values: string[]; +}; + +/** WebSocket message types exchanged with the Browser MCP Chrome extension. */ +export type SocketMessageMap = { + getUrl: { payload: undefined; result: string }; + getTitle: { payload: undefined; result: string }; + browser_snapshot: { payload: EmptyPayload; result: string }; + browser_navigate: { payload: { url: string }; result: void }; + browser_go_back: { payload: EmptyPayload; result: void }; + browser_go_forward: { payload: EmptyPayload; result: void }; + browser_wait: { payload: { time: number }; result: void }; + browser_press_key: { payload: { key: string }; result: void }; + browser_click: { payload: ElementInteractionPayload; result: void }; + browser_drag: { payload: DragPayload; result: void }; + browser_hover: { payload: ElementInteractionPayload; result: void }; + browser_type: { payload: TypePayload; result: void }; + browser_select_option: { payload: SelectOptionPayload; result: void }; + browser_screenshot: { payload: EmptyPayload; result: string }; + browser_get_console_logs: { payload: EmptyPayload; result: ConsoleLog[] }; +}; diff --git a/src/utils/aria-snapshot.ts b/src/utils/aria-snapshot.ts index de763f1f..ff3b52ac 100644 --- a/src/utils/aria-snapshot.ts +++ b/src/utils/aria-snapshot.ts @@ -5,9 +5,11 @@ export async function captureAriaSnapshot( context: Context, status: string = "", ): Promise { - const url = await context.sendSocketMessage("getUrl", undefined); - const title = await context.sendSocketMessage("getTitle", undefined); - const snapshot = await context.sendSocketMessage("browser_snapshot", {}); + const [url, title, snapshot] = await Promise.all([ + context.sendSocketMessage("getUrl", undefined), + context.sendSocketMessage("getTitle", undefined), + context.sendSocketMessage("browser_snapshot", {}), + ]); return { content: [ { diff --git a/src/utils/id.ts b/src/utils/id.ts new file mode 100644 index 00000000..a799419e --- /dev/null +++ b/src/utils/id.ts @@ -0,0 +1,8 @@ +export function generateRequestId(): string { + if (typeof globalThis.crypto?.randomUUID === "function") { + return globalThis.crypto.randomUUID(); + } + const timestamp = Date.now().toString(36); + const randomStr = Math.random().toString(36).substring(2, 10); + return `${timestamp}-${randomStr}`; +} diff --git a/src/utils/port.ts b/src/utils/port.ts index 0e388dfe..e38bbded 100644 --- a/src/utils/port.ts +++ b/src/utils/port.ts @@ -1,27 +1,55 @@ import { execSync } from "node:child_process"; import net from "node:net"; -export async function isPortInUse(port: number): Promise { +import { mcpConfig } from "@/config/mcp.config"; +import { assertAllowedPort } from "@/security/validation"; + +const ALLOWED_KILL_PORTS: ReadonlySet = new Set([ + mcpConfig.defaultWsPort, + mcpConfig.bridgeControlPort, +]); + +export async function isPortInUse( + port: number, + host = "127.0.0.1", +): Promise { + assertAllowedPort(port); + return new Promise((resolve) => { - const server = net.createServer(); - server.once("error", () => resolve(true)); // Port is still in use - server.once("listening", () => { - server.close(() => resolve(false)); // Port is free + const socket = net.createConnection({ port, host }); + socket.setTimeout(500); + socket.once("connect", () => { + socket.destroy(); + resolve(true); + }); + socket.once("timeout", () => { + socket.destroy(); + resolve(false); }); - server.listen(port); + socket.once("error", () => resolve(false)); }); } -export function killProcessOnPort(port: number) { +export function killProcessOnPort(port: number): void { + assertAllowedPort(port); + + if (!ALLOWED_KILL_PORTS.has(port)) { + throw new Error(`Refusing to kill processes on port ${port}`); + } + try { if (process.platform === "win32") { execSync( `FOR /F "tokens=5" %a in ('netstat -ano ^| findstr :${port}') do taskkill /F /PID %a`, + { stdio: "ignore" }, ); } else { - execSync(`lsof -ti:${port} | xargs kill -9`); + execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { + shell: "/bin/sh", + stdio: "ignore", + }); } - } catch (error) { - console.error(`Failed to kill process on port ${port}:`, error); + } catch { + // No process on port — expected when the port is already free. } } diff --git a/src/utils/wait.ts b/src/utils/wait.ts new file mode 100644 index 00000000..c5175788 --- /dev/null +++ b/src/utils/wait.ts @@ -0,0 +1,5 @@ +export async function wait(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve(), ms); + }); +} diff --git a/src/ws-daemon.ts b/src/ws-daemon.ts new file mode 100644 index 00000000..28dddf50 --- /dev/null +++ b/src/ws-daemon.ts @@ -0,0 +1,19 @@ +import { startBridgeDaemon } from "@/bridge/daemon"; +import { mcpConfig } from "@/config/mcp.config"; + +const wsPort = Number(process.env.BROWSERMCP_WS_PORT ?? mcpConfig.defaultWsPort); +const controlPort = Number( + process.env.BROWSERMCP_CONTROL_PORT ?? mcpConfig.bridgeControlPort, +); + +const daemon = await startBridgeDaemon(wsPort, controlPort); + +process.on("SIGINT", async () => { + await daemon.close(); + process.exit(0); +}); + +process.on("SIGTERM", async () => { + await daemon.close(); + process.exit(0); +}); diff --git a/src/ws.ts b/src/ws.ts index fb9f25ae..62d6c444 100644 --- a/src/ws.ts +++ b/src/ws.ts @@ -1,17 +1,31 @@ import { WebSocketServer } from "ws"; -import { mcpConfig } from "@repo/config/mcp.config"; -import { wait } from "@repo/utils"; +import { mcpConfig } from "@/config/mcp.config"; +import { isPortInUse } from "@/utils/port"; +import { wait } from "@/utils/wait"; -import { isPortInUse, killProcessOnPort } from "@/utils/port"; +const PORT_WAIT_INTERVAL_MS = 100; +const PORT_WAIT_MAX_ATTEMPTS = 50; export async function createWebSocketServer( port: number = mcpConfig.defaultWsPort, ): Promise { - killProcessOnPort(port); - // Wait until the port is free - while (await isPortInUse(port)) { - await wait(100); + for (let attempt = 0; attempt < PORT_WAIT_MAX_ATTEMPTS; attempt++) { + if (!(await isPortInUse(port))) { + break; + } + if (attempt === PORT_WAIT_MAX_ATTEMPTS - 1) { + throw new Error( + `Port ${port} is still in use after ${(PORT_WAIT_MAX_ATTEMPTS * PORT_WAIT_INTERVAL_MS) / 1000}s`, + ); + } + await wait(PORT_WAIT_INTERVAL_MS); } - return new WebSocketServer({ port }); + + const wss = new WebSocketServer({ port, host: "127.0.0.1" }); + await new Promise((resolve, reject) => { + wss.once("listening", () => resolve()); + wss.once("error", reject); + }); + return wss; } diff --git a/test/pairing.test.ts b/test/pairing.test.ts new file mode 100644 index 00000000..c63cc4b3 --- /dev/null +++ b/test/pairing.test.ts @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, it } from "node:test"; + +import { installServerConfig } from "../src/pairing/install.js"; +import { createCursorInstallLink, createVsCodeInstallLink } from "../src/pairing/links.js"; +import { resolveServerConfig } from "../src/pairing/server-config.js"; +import { SERVER_NAME } from "../src/pairing/types.js"; + +describe("pairing", () => { + it("creates a Cursor install deeplink with base64 config", () => { + const config = { + command: "npx", + args: ["-y", "@browsermcp/mcp@0.2.1"], + }; + const link = createCursorInstallLink(config); + + assert.match(link, /^cursor:\/\/anysphere\.cursor-deeplink\/mcp\/install\?/); + assert.match(link, /name=browsermcp/); + assert.match(link, /config=/); + + const configParam = new URL(link.replace("cursor://", "https://")).searchParams.get("config"); + assert.ok(configParam); + const decoded = JSON.parse(Buffer.from(configParam, "base64url").toString("utf8")) as { + command: string; + args: string[]; + }; + assert.deepEqual(decoded, config); + }); + + it("creates a VS Code install deeplink", () => { + const config = { + command: "npx", + args: ["-y", "@browsermcp/mcp@0.2.1"], + }; + const link = createVsCodeInstallLink(config); + + assert.match(link, /^vscode:\/\/mcp\/install\?/); + const payload = decodeURIComponent(link.replace("vscode://mcp/install?", "")); + const parsed = JSON.parse(payload) as { + name: string; + type: string; + command: string; + args: string[]; + }; + assert.equal(parsed.name, SERVER_NAME); + assert.equal(parsed.type, "stdio"); + assert.equal(parsed.command, config.command); + assert.deepEqual(parsed.args, config.args); + }); + + it("merges browsermcp into an existing mcp.json idempotently", () => { + const dir = mkdtempSync(path.join(os.tmpdir(), "browsermcp-pair-")); + const configPath = path.join(dir, "mcp.json"); + + const serverConfig = resolveServerConfig({ local: true }); + + const first = installServerConfig( + { id: "cursor", label: "Cursor", configPath }, + serverConfig, + ); + assert.equal(first.updated, true); + + const written = JSON.parse(readFileSync(configPath, "utf8")) as { + mcpServers: Record; + }; + assert.ok(written.mcpServers[SERVER_NAME]); + assert.equal(written.mcpServers[SERVER_NAME].command, serverConfig.command); + + const second = installServerConfig( + { id: "cursor", label: "Cursor", configPath }, + serverConfig, + ); + assert.equal(second.updated, false); + + rmSync(dir, { recursive: true, force: true }); + }); +}); diff --git a/test/security.test.ts b/test/security.test.ts new file mode 100644 index 00000000..0324f721 --- /dev/null +++ b/test/security.test.ts @@ -0,0 +1,119 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import { describe, it } from "node:test"; +import { WebSocket } from "ws"; + +import { BridgeClient } from "../src/bridge/client.js"; +import { startBridgeDaemon } from "../src/bridge/daemon.js"; +import { encodeBridgeMessage } from "../src/bridge/protocol.js"; +import { mcpConfig } from "../src/config/mcp.config.js"; +import { MESSAGE_RESPONSE_TYPE } from "../src/messaging/ws/types.js"; +import { ensureBridgeToken, tokensMatch } from "../src/security/token.js"; +import { + assertAllowedPort, + assertSafeNavigationUrl, +} from "../src/security/validation.js"; +import { NavigateTool } from "../src/types/mcp/tool.js"; +import { killProcessOnPort } from "../src/utils/port.js"; + +function randomPort(): number { + return 20_000 + Math.floor(Math.random() * 10_000); +} + +describe("security validation", () => { + it("allows http and https URLs", () => { + assert.doesNotThrow(() => assertSafeNavigationUrl("https://example.com/path")); + assert.doesNotThrow(() => assertSafeNavigationUrl("http://localhost:3000")); + }); + + it("blocks dangerous URL protocols", () => { + assert.throws( + () => assertSafeNavigationUrl("javascript:alert(1)"), + /Blocked URL protocol/, + ); + assert.throws(() => assertSafeNavigationUrl("file:///etc/passwd"), /Blocked URL protocol/); + assert.throws(() => assertSafeNavigationUrl("data:text/html,hello"), /Blocked URL protocol/); + }); + + it("rejects invalid ports", () => { + assert.throws(() => assertAllowedPort(0), /Invalid port/); + assert.throws(() => assertAllowedPort(99_999), /Invalid port/); + }); + + it("NavigateTool rejects javascript URLs", () => { + assert.throws( + () => NavigateTool.shape.arguments.parse({ url: "javascript:alert(1)" }), + /Blocked URL protocol/, + ); + }); + + it("compares tokens in constant time", () => { + assert.equal(tokensMatch("secret-token", "secret-token"), true); + assert.equal(tokensMatch("secret-token", "secret-tokex"), false); + assert.equal(tokensMatch("short", "longer-value"), false); + }); +}); + +describe("bridge authentication", () => { + it("requires a valid token on the control channel", async () => { + const wsPort = randomPort(); + const controlPort = randomPort(); + const token = await ensureBridgeToken(); + const daemon = await startBridgeDaemon(wsPort, controlPort); + + const wss = new WebSocket(`ws://127.0.0.1:${wsPort}`); + await new Promise((resolve, reject) => { + wss.once("open", () => resolve()); + wss.once("error", reject); + }); + + wss.on("message", (raw) => { + const request = JSON.parse(raw.toString()) as { id: string }; + wss.send( + JSON.stringify({ + type: MESSAGE_RESPONSE_TYPE, + payload: { requestId: request.id, result: "ok" }, + }), + ); + }); + + try { + const client = await BridgeClient.connect(controlPort); + const result = await client.sendSocketMessage("browser_wait", { time: 0.01 }); + assert.equal(result, "ok"); + await client.close(); + + await assert.rejects( + () => + new Promise((resolve, reject) => { + const socket = net.createConnection({ host: "127.0.0.1", port: controlPort }); + socket.once("connect", () => { + socket.write(encodeBridgeMessage({ kind: "auth", token: "wrong-token" })); + }); + socket.on("data", (chunk) => { + const payload = JSON.parse(chunk.toString()) as { ok?: boolean }; + if (payload.ok === false) { + socket.destroy(); + reject(new Error("auth rejected")); + } else { + resolve(); + } + }); + socket.once("error", reject); + }), + /auth rejected/, + ); + + assert.equal(token.length >= 16, true); + } finally { + wss.close(); + await daemon.close(); + } + }); +}); + +describe("port utilities", () => { + it("refuses to kill processes on arbitrary ports", () => { + assert.throws(() => killProcessOnPort(randomPort()), /Refusing to kill processes/); + }); +}); diff --git a/test/smoke.test.ts b/test/smoke.test.ts new file mode 100644 index 00000000..39110ec0 --- /dev/null +++ b/test/smoke.test.ts @@ -0,0 +1,292 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { describe, it } from "node:test"; +import { WebSocket, WebSocketServer } from "ws"; + +import { Context } from "../src/context.js"; +import { mcpConfig } from "../src/config/mcp.config.js"; +import { MESSAGE_RESPONSE_TYPE } from "../src/messaging/ws/types.js"; +import { createSocketMessageSender } from "../src/messaging/ws/sender.js"; +import { createServerWithTools } from "../src/server.js"; +import { + ClickTool, + NavigateTool, + SnapshotTool, + TypeTool, +} from "../src/types/mcp/tool.js"; +import { isPortInUse, killProcessOnPort } from "../src/utils/port.js"; +import { createWebSocketServer } from "../src/ws.js"; + +function randomPort(): number { + return 20_000 + Math.floor(Math.random() * 10_000); +} + +describe("build artifact", () => { + it("dist/index.js has a single shebang and is non-empty", () => { + const dist = readFileSync(new URL("../dist/index.js", import.meta.url), "utf8"); + assert.match(dist, /^#!\/usr\/bin\/env node\n/); + assert.equal((dist.match(/^#!\/usr\/bin\/env node\n/gm) ?? []).length, 1); + assert.ok(dist.length > 10_000); + }); +}); + +describe("tool schemas", () => { + it("parses navigate arguments", () => { + const parsed = NavigateTool.shape.arguments.parse({ url: "https://example.com" }); + assert.equal(parsed.url, "https://example.com"); + }); + + it("parses click arguments", () => { + const parsed = ClickTool.shape.arguments.parse({ + element: "Submit button", + ref: "e42", + }); + assert.equal(parsed.ref, "e42"); + }); + + it("parses type arguments with submit flag", () => { + const parsed = TypeTool.shape.arguments.parse({ + element: "Search", + ref: "e1", + text: "hello", + submit: true, + }); + assert.equal(parsed.text, "hello"); + assert.equal(parsed.submit, true); + }); + + it("snapshot tool has empty arguments object", () => { + assert.deepEqual(SnapshotTool.shape.arguments.parse({}), {}); + }); +}); + +describe("port utilities", () => { + it("isPortInUse returns false for a free port", async () => { + const port = randomPort(); + assert.equal(await isPortInUse(port), false); + }); + + it("isPortInUse returns true while a server is listening", async () => { + const port = randomPort(); + const wss = new WebSocketServer({ port, host: "127.0.0.1" }); + await new Promise((resolve) => wss.once("listening", resolve)); + try { + assert.equal(await isPortInUse(port), true); + } finally { + await new Promise((resolve, reject) => + wss.close((err) => (err ? reject(err) : resolve())), + ); + } + }); +}); + +describe("WebSocket messaging", () => { + it("correlates request/response by id", async () => { + const wss = new WebSocketServer({ port: 0 }); + const port = await new Promise((resolve) => { + wss.once("listening", () => { + const address = wss.address(); + resolve(typeof address === "object" && address ? address.port : 0); + }); + }); + + wss.on("connection", (ws) => { + ws.on("message", (raw) => { + const request = JSON.parse(raw.toString()) as { + id: string; + type: string; + payload: { value: number }; + }; + ws.send( + JSON.stringify({ + type: MESSAGE_RESPONSE_TYPE, + payload: { + requestId: request.id, + result: request.payload.value * 2, + }, + }), + ); + }); + }); + + const client = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve, reject) => { + client.once("open", () => resolve()); + client.once("error", reject); + }); + + try { + const { sendSocketMessage } = createSocketMessageSender(client); + const result = await sendSocketMessage<{ value: number }, number>( + "test_echo", + { value: 21 }, + { timeoutMs: 2000 }, + ); + assert.equal(result, 42); + } finally { + client.close(); + await new Promise((resolve) => + wss.close(() => resolve()), + ); + } + }); + + it("rejects when the socket is not open", async () => { + const wss = new WebSocketServer({ port: 0 }); + const port = await new Promise((resolve) => { + wss.once("listening", () => { + const address = wss.address(); + resolve(typeof address === "object" && address ? address.port : 0); + }); + }); + + const client = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve, reject) => { + client.once("open", () => resolve()); + client.once("error", reject); + }); + client.close(); + await new Promise((resolve) => client.once("close", () => resolve())); + + const { sendSocketMessage } = createSocketMessageSender(client); + await assert.rejects( + () => sendSocketMessage("test", {}, { timeoutMs: 500 }), + /WebSocket is not open/, + ); + + await new Promise((resolve) => + wss.close(() => resolve()), + ); + }); + + it("rejects on response error payload", async () => { + const wss = new WebSocketServer({ port: 0 }); + const port = await new Promise((resolve) => { + wss.once("listening", () => { + const address = wss.address(); + resolve(typeof address === "object" && address ? address.port : 0); + }); + }); + + wss.on("connection", (ws) => { + ws.on("message", (raw) => { + const request = JSON.parse(raw.toString()) as { id: string }; + ws.send( + JSON.stringify({ + type: MESSAGE_RESPONSE_TYPE, + payload: { requestId: request.id, error: "No tab is connected" }, + }), + ); + }); + }); + + const client = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve, reject) => { + client.once("open", () => resolve()); + client.once("error", reject); + }); + + try { + const { sendSocketMessage } = createSocketMessageSender(client); + await assert.rejects( + () => sendSocketMessage("browser_navigate", { url: "https://x.test" }), + /No tab is connected/, + ); + } finally { + client.close(); + await new Promise((resolve) => + wss.close(() => resolve()), + ); + } + }); +}); + +describe("Context", () => { + it("throws a helpful error when no extension is connected", () => { + const context = new Context(); + assert.throws(() => context.ws, /No connection to browser extension/); + }); + + it("maps noConnectedTab errors to the extension message", async () => { + const wss = new WebSocketServer({ port: 0 }); + const port = await new Promise((resolve) => { + wss.once("listening", () => { + const address = wss.address(); + resolve(typeof address === "object" && address ? address.port : 0); + }); + }); + + wss.on("connection", (ws) => { + ws.on("message", (raw) => { + const request = JSON.parse(raw.toString()) as { id: string }; + ws.send( + JSON.stringify({ + type: MESSAGE_RESPONSE_TYPE, + payload: { + requestId: request.id, + error: mcpConfig.errors.noConnectedTab, + }, + }), + ); + }); + }); + + const client = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve, reject) => { + client.once("open", () => resolve()); + client.once("error", reject); + }); + + const context = new Context(); + context.ws = client; + + try { + await assert.rejects( + () => context.sendSocketMessage("getUrl", undefined), + /No connection to browser extension/, + ); + } finally { + await context.close(); + await new Promise((resolve) => + wss.close(() => resolve()), + ); + } + }); +}); + +describe("MCP server lifecycle", () => { + it("createWebSocketServer binds to a custom port", async () => { + const port = randomPort(); + const wss = await createWebSocketServer(port); + try { + assert.equal(await isPortInUse(port), true); + } finally { + await new Promise((resolve, reject) => + wss.close((err) => (err ? reject(err) : resolve())), + ); + } + }); + + it("server.close() completes without hanging", async () => { + const port = randomPort(); + const server = await createServerWithTools({ + name: "test", + version: "0.0.0", + tools: [], + resources: [], + wsPort: port, + useBridge: false, + }); + + await assert.doesNotReject(async () => { + await Promise.race([ + server.close(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("server.close() timed out")), 5000), + ), + ]); + }); + + assert.equal(await isPortInUse(port), false); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index d466d958..9450973a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,12 +8,13 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, + "lib": ["ES2022"], + "types": ["node"], "paths": { "@/*": ["./src/*"] } }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] + "include": ["src/**/*.ts", "tsup.config.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 00000000..58969d0b --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/ws-daemon.ts"], + format: ["esm"], + target: "node18", + platform: "node", + clean: true, + dts: false, + sourcemap: false, + banner: { + js: "#!/usr/bin/env node", + }, +});