From 7c033b6118f6d3166e64679097feec04ae257937 Mon Sep 17 00:00:00 2001 From: cneicy Date: Sat, 15 Aug 2026 20:38:50 +0800 Subject: [PATCH] feat: add CraftTable MCP OAuth CLI --- .gitignore | 4 + README.md | 37 + package-lock.json | 2503 +++++++++++++++++++++++++++++++++ package.json | 42 + src/agents.ts | 264 ++++ src/bridge.ts | 60 + src/callbackServer.ts | 84 ++ src/cli.ts | 140 ++ src/config.ts | 112 ++ src/credentials.ts | 64 + src/oauthProvider.ts | 84 ++ src/remote.ts | 167 +++ test/agents.test.ts | 75 + test/bridge.test.ts | 56 + test/configAndOAuth.test.ts | 128 ++ test/oauthIntegration.test.ts | 159 +++ test/stdioFixture.ts | 12 + test/stdioProtocol.test.ts | 48 + tsconfig.json | 14 + 19 files changed, 4053 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/agents.ts create mode 100644 src/bridge.ts create mode 100644 src/callbackServer.ts create mode 100644 src/cli.ts create mode 100644 src/config.ts create mode 100644 src/credentials.ts create mode 100644 src/oauthProvider.ts create mode 100644 src/remote.ts create mode 100644 test/agents.test.ts create mode 100644 test/bridge.test.ts create mode 100644 test/configAndOAuth.test.ts create mode 100644 test/oauthIntegration.test.ts create mode 100644 test/stdioFixture.ts create mode 100644 test/stdioProtocol.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68b747e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +coverage/ +*.tgz diff --git a/README.md b/README.md new file mode 100644 index 0000000..303f45e --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# CraftTable MCP CLI + +`@game-crafttable/mcp-client` provides the `crafttable-mcp` command and the compatibility alias `crafttable-mcp-client`. + +## Quick install and configure + +The repository is private, so first make sure Git Credential Manager can access `git.crash.work`, then run this in PowerShell: + +```powershell +npm install --global git+https://git.crash.work/cneicy/crafttable-mcp-client.git; crafttable-mcp login; crafttable-mcp configure all +``` + +This installs the CLI, opens the browser for OAuth login, and configures the same local stdio bridge for Codex, Claude Code, and OpenCode at user scope. + +## Commands + +```powershell +crafttable-mcp login +crafttable-mcp status +crafttable-mcp tools +crafttable-mcp call list_spaces '{}' +crafttable-mcp configure all +``` + +`login` uses Authorization Code with PKCE and saves OAuth tokens in the operating-system credential store. Tokens are isolated by MCP server URL and OAuth client ID. The CLI never falls back to a plaintext token file. + +`serve` is a stdio MCP bridge for Codex, Claude Code, and OpenCode. It writes MCP JSON-RPC only to stdout. If no OAuth credential exists, it can use the legacy `CRAFTTABLE_MCP_TOKEN` environment variable and reports that path only on stderr. + +Connection defaults: + +- URL: `https://crafttable.crash.work/mcp` +- OAuth client ID: `crafttable-mcp-cli` +- callback: `http://127.0.0.1:48321/oauth/callback` + +Override them with `--url`, `--client-id`, and `--callback-port`, or with `CRAFTTABLE_MCP_URL`, `CRAFTTABLE_MCP_OAUTH_CLIENT_ID`, and `CRAFTTABLE_MCP_OAUTH_CALLBACK_PORT`. + +Only HTTPS remote URLs are accepted. Plain HTTP is limited to `localhost`, `127.0.0.1`, and `::1` development servers. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..66fca4d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2503 @@ +{ + "name": "@game-crafttable/mcp-client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@game-crafttable/mcp-client", + "version": "0.1.0", + "license": "UNLICENSED", + "dependencies": { + "@modelcontextprotocol/sdk": "latest", + "@napi-rs/keyring": "^1.3.0", + "commander": "^14.0.0", + "jsonc-parser": "^3.3.1", + "open": "^10.2.0" + }, + "bin": { + "crafttable-mcp": "dist/cli.js", + "crafttable-mcp-client": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "latest", + "esbuild": "latest", + "tsx": "latest", + "typescript": "latest" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@napi-rs/keyring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", + "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5ac47ff --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "@game-crafttable/mcp-client", + "version": "0.1.0", + "description": "OAuth CLI and stdio bridge for the CraftTable MCP server", + "repository": { + "type": "git", + "url": "https://git.crash.work/cneicy/crafttable-mcp-client.git" + }, + "type": "module", + "bin": { + "crafttable-mcp": "dist/cli.js", + "crafttable-mcp-client": "dist/cli.js" + }, + "files": [ + "dist", + "README.md" + ], + "engines": { + "node": ">=20" + }, + "scripts": { + "dev": "tsx src/cli.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "npm run typecheck && esbuild src/cli.ts --bundle --platform=node --format=esm --packages=external --sourcemap --outfile=dist/cli.js", + "prepare": "npm run build", + "test": "tsx --test test/*.test.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "latest", + "@napi-rs/keyring": "^1.3.0", + "commander": "^14.0.0", + "jsonc-parser": "^3.3.1", + "open": "^10.2.0" + }, + "devDependencies": { + "@types/node": "latest", + "esbuild": "latest", + "tsx": "latest", + "typescript": "latest" + }, + "license": "UNLICENSED" +} diff --git a/src/agents.ts b/src/agents.ts new file mode 100644 index 0000000..66f480f --- /dev/null +++ b/src/agents.ts @@ -0,0 +1,264 @@ +import { spawn } from "node:child_process"; +import { constants } from "node:fs"; +import { copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createInterface } from "node:readline/promises"; +import { applyEdits, modify, parse } from "jsonc-parser"; +import type { ClientOptions } from "./config.js"; +import { errorMessage } from "./config.js"; + +export const SERVER_NAME = "crafttable"; +export type AgentName = "codex" | "claude" | "opencode"; +export type AgentTarget = AgentName | "all"; + +export type CommandResult = { code: number; stdout: string; stderr: string }; +export interface CommandRunner { + run(command: string, args: string[]): Promise; +} + +export type ConfigureInput = { + target: AgentTarget; + options: ClientOptions; + cliEntry: string; + nodePath?: string; + dryRun?: boolean; + force?: boolean; + env?: NodeJS.ProcessEnv; + runner?: CommandRunner; + confirm?: (message: string) => Promise; + opencodePath?: string; +}; + +export type ConfigureResult = { + agent: AgentName; + action: "added" | "replaced" | "removed" | "unchanged" | "absent" | "would-add" | "would-replace" | "would-remove"; +}; + +export async function configureAgents(input: ConfigureInput): Promise { + const agents = expandTarget(input.target); + const command = launchCommand(input.options, input.cliEntry, input.nodePath ?? process.execPath); + const runner = input.runner ?? new SpawnCommandRunner(); + const results: ConfigureResult[] = []; + for (const agent of agents) { + if (agent === "opencode") { + results.push(await configureOpenCode(input, command)); + } else { + results.push(await configureCliAgent(agent, command, input, runner)); + } + } + return results; +} + +export async function unconfigureAgents(input: ConfigureInput): Promise { + const agents = expandTarget(input.target); + const runner = input.runner ?? new SpawnCommandRunner(); + const results: ConfigureResult[] = []; + for (const agent of agents) { + if (agent === "opencode") { + results.push(await unconfigureOpenCode(input)); + continue; + } + const executable = agent === "codex" ? "codex" : "claude"; + const existing = await probeCliAgent(agent, runner); + if (!existing.exists) { + results.push({ agent, action: "absent" }); + continue; + } + if (input.dryRun) { + results.push({ agent, action: "would-remove" }); + continue; + } + const args = agent === "codex" + ? ["mcp", "remove", SERVER_NAME] + : ["mcp", "remove", "--scope", "user", SERVER_NAME]; + await requireSuccess(runner.run(executable, args), `${agent} MCP removal`); + results.push({ agent, action: "removed" }); + } + return results; +} + +export function launchCommand(options: ClientOptions, cliEntry: string, nodePath: string): string[] { + return [ + path.resolve(nodePath), + path.resolve(cliEntry), + "serve", + "--url", options.url.toString(), + "--client-id", options.clientId, + "--callback-port", String(options.callbackPort), + ]; +} + +async function configureCliAgent( + agent: "codex" | "claude", + command: string[], + input: ConfigureInput, + runner: CommandRunner, +): Promise { + const existing = await probeCliAgent(agent, runner); + if (existing.exists && outputMatchesCommand(existing.output, command)) return { agent, action: "unchanged" }; + if (existing.exists && !input.force && !input.dryRun) { + const confirm = input.confirm ?? terminalConfirm; + if (!process.stdin.isTTY && !input.confirm) throw new Error(`${agent} already has a different ${SERVER_NAME} MCP entry; use --force to replace it`); + if (!await confirm(`${agent} already has a different ${SERVER_NAME} MCP entry. Replace it?`)) { + throw new Error(`${agent} MCP configuration was not changed`); + } + } + const action = existing.exists ? "replaced" : "added"; + if (input.dryRun) return { agent, action: existing.exists ? "would-replace" : "would-add" }; + const executable = agent === "codex" ? "codex" : "claude"; + if (existing.exists) { + const removeArgs = agent === "codex" + ? ["mcp", "remove", SERVER_NAME] + : ["mcp", "remove", "--scope", "user", SERVER_NAME]; + await requireSuccess(runner.run(executable, removeArgs), `${agent} MCP replacement cleanup`); + } + const addArgs = agent === "codex" + ? ["mcp", "add", SERVER_NAME, "--", ...command] + : ["mcp", "add", "--scope", "user", SERVER_NAME, "--", ...command]; + await requireSuccess(runner.run(executable, addArgs), `${agent} MCP registration`); + return { agent, action }; +} + +async function probeCliAgent(agent: "codex" | "claude", runner: CommandRunner): Promise<{ exists: boolean; output: string }> { + const executable = agent === "codex" ? "codex" : "claude"; + const args = agent === "codex" + ? ["mcp", "get", SERVER_NAME, "--json"] + : ["mcp", "get", SERVER_NAME]; + const result = await runner.run(executable, args); + if (result.code === 0) return { exists: true, output: result.stdout }; + const combined = `${result.stdout}\n${result.stderr}`; + if (/not found|does not exist|no mcp server|not configured|unknown server/i.test(combined)) return { exists: false, output: combined }; + throw new Error(`Could not inspect ${agent} MCP configuration: ${safeCommandError(result)}`); +} + +function outputMatchesCommand(output: string, command: string[]): boolean { + try { + const document = JSON.parse(output) as unknown; + if (findCommand(document, command)) return true; + } catch { + // Claude currently returns a human-readable record. + } + return command.every((part) => output.includes(part)); +} + +function findCommand(value: unknown, command: string[]): boolean { + if (!value || typeof value !== "object") return false; + if (Array.isArray(value)) return value.some((item) => findCommand(item, command)); + const record = value as Record; + if (typeof record.command === "string" && Array.isArray(record.args)) { + const candidate = [record.command, ...record.args.filter((item): item is string => typeof item === "string")]; + if (candidate.length === command.length && candidate.every((item, index) => item === command[index])) return true; + } + return Object.values(record).some((item) => findCommand(item, command)); +} + +async function configureOpenCode(input: ConfigureInput, command: string[]): Promise { + const filePath = input.opencodePath ?? defaultOpenCodePath(input.env); + const original = await readOptionalFile(filePath) ?? "{}\n"; + const document = parse(original) as { mcp?: Record } | undefined; + const existing = document?.mcp?.[SERVER_NAME] as { type?: unknown; command?: unknown; enabled?: unknown } | undefined; + const desired = { type: "local", command, enabled: true }; + if (existing && existing.type === desired.type && existing.enabled === true && arraysEqual(existing.command, command)) { + return { agent: "opencode", action: "unchanged" }; + } + if (existing && !input.force && !input.dryRun) { + const confirm = input.confirm ?? terminalConfirm; + if (!process.stdin.isTTY && !input.confirm) throw new Error(`opencode already has a different ${SERVER_NAME} MCP entry; use --force to replace it`); + if (!await confirm(`opencode already has a different ${SERVER_NAME} MCP entry. Replace it?`)) { + throw new Error("opencode MCP configuration was not changed"); + } + } + if (input.dryRun) return { agent: "opencode", action: existing ? "would-replace" : "would-add" }; + const updated = applyEdits(original, modify(original, ["mcp", SERVER_NAME], desired, { + formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }, + })); + await backupAndAtomicWrite(filePath, updated, await readOptionalFile(filePath) !== undefined); + return { agent: "opencode", action: existing ? "replaced" : "added" }; +} + +async function unconfigureOpenCode(input: ConfigureInput): Promise { + const filePath = input.opencodePath ?? defaultOpenCodePath(input.env); + const original = await readOptionalFile(filePath); + if (original === undefined) return { agent: "opencode", action: "absent" }; + const document = parse(original) as { mcp?: Record } | undefined; + if (!document?.mcp || !(SERVER_NAME in document.mcp)) return { agent: "opencode", action: "absent" }; + if (input.dryRun) return { agent: "opencode", action: "would-remove" }; + const updated = applyEdits(original, modify(original, ["mcp", SERVER_NAME], undefined, { + formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }, + })); + await backupAndAtomicWrite(filePath, updated, true); + return { agent: "opencode", action: "removed" }; +} + +export function defaultOpenCodePath(env: NodeJS.ProcessEnv = process.env): string { + const base = env.XDG_CONFIG_HOME || (process.platform === "win32" + ? path.join(env.USERPROFILE || os.homedir(), ".config") + : path.join(os.homedir(), ".config")); + return path.join(base, "opencode", "opencode.json"); +} + +async function backupAndAtomicWrite(filePath: string, updated: string, existed: boolean): Promise { + await mkdir(path.dirname(filePath), { recursive: true }); + if (existed) { + try { + await copyFile(filePath, `${filePath}.crafttable-mcp.backup`, constants.COPYFILE_EXCL); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; + } + } + const temporary = `${filePath}.${process.pid}.tmp`; + await writeFile(temporary, updated, "utf8"); + await rename(temporary, filePath); +} + +async function readOptionalFile(filePath: string): Promise { + try { + return await readFile(filePath, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + throw error; + } +} + +function arraysEqual(value: unknown, expected: string[]): boolean { + return Array.isArray(value) && value.length === expected.length && value.every((item, index) => item === expected[index]); +} + +function expandTarget(target: AgentTarget): AgentName[] { + if (target === "all") return ["codex", "claude", "opencode"]; + if (["codex", "claude", "opencode"].includes(target)) return [target as AgentName]; + throw new Error("Agent must be one of: codex, claude, opencode, all"); +} + +async function terminalConfirm(message: string): Promise { + const readline = createInterface({ input: process.stdin, output: process.stderr }); + try { + return /^y(es)?$/i.test((await readline.question(`${message} [y/N] `)).trim()); + } finally { + readline.close(); + } +} + +async function requireSuccess(resultPromise: Promise, operation: string): Promise { + const result = await resultPromise; + if (result.code !== 0) throw new Error(`${operation} failed: ${safeCommandError(result)}`); +} + +function safeCommandError(result: CommandResult): string { + return (result.stderr || result.stdout || `exit code ${result.code}`).trim(); +} + +export class SpawnCommandRunner implements CommandRunner { + run(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += String(chunk); }); + child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += String(chunk); }); + child.once("error", (error) => reject(new Error(`Could not run ${command}: ${errorMessage(error)}`))); + child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr })); + }); + } +} diff --git a/src/bridge.ts b/src/bridge.ts new file mode 100644 index 0000000..836e8bc --- /dev/null +++ b/src/bridge.ts @@ -0,0 +1,60 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import type { ClientOptions } from "./config.js"; +import { DiscoveryStore } from "./config.js"; +import type { TokenStore } from "./credentials.js"; +import { connectRemote } from "./remote.js"; + +export async function serveBridge( + options: ClientOptions, + tokenStore: TokenStore, + discoveryStore: DiscoveryStore, + serviceToken = process.env.CRAFTTABLE_MCP_TOKEN?.trim(), +): Promise { + const upstream = await connectRemote(options, tokenStore, discoveryStore, serviceToken); + if (upstream.authentication === "service-token") { + process.stderr.write("CraftTable MCP: using the legacy service-account token path.\n"); + } + const server = createProxyServer(upstream.client); + const transport = new StdioServerTransport(); + const close = async (): Promise => { + await server.close().catch(() => undefined); + await upstream.close().catch(() => undefined); + }; + process.once("SIGINT", () => void close()); + process.once("SIGTERM", () => void close()); + try { + await server.connect(transport); + } catch (error) { + await close(); + throw error; + } +} + +export function createProxyServer(upstream: { + listTools: (params?: { cursor?: string }) => Promise; + callTool: (params: { name: string; arguments?: Record }) => Promise; + listResources: (params?: { cursor?: string }) => Promise; + listResourceTemplates: (params?: { cursor?: string }) => Promise; + readResource: (params: { uri: string }) => Promise; +}): Server { + const server = new Server({ name: "crafttable-mcp-stdio-bridge", version: "0.1.0" }, { + capabilities: { + tools: {}, + resources: {}, + }, + }); + server.setRequestHandler(ListToolsRequestSchema, (request) => upstream.listTools(request.params) as never); + server.setRequestHandler(CallToolRequestSchema, (request) => upstream.callTool(request.params) as never); + server.setRequestHandler(ListResourcesRequestSchema, (request) => upstream.listResources(request.params) as never); + server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => upstream.listResourceTemplates(request.params) as never); + server.setRequestHandler(ReadResourceRequestSchema, (request) => upstream.readResource(request.params) as never); + return server; +} diff --git a/src/callbackServer.ts b/src/callbackServer.ts new file mode 100644 index 0000000..20dab5b --- /dev/null +++ b/src/callbackServer.ts @@ -0,0 +1,84 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +export type OAuthCallback = { code: string }; + +export class OAuthCallbackServer { + private server?: Server; + private resolveResult?: (value: OAuthCallback) => void; + private rejectResult?: (reason: Error) => void; + private readonly result = new Promise((resolve, reject) => { + this.resolveResult = resolve; + this.rejectResult = reject; + }); + + constructor( + private readonly port: number, + private readonly validateState: (state: string | null) => boolean, + ) { + // A very fast browser callback can arrive before loginRemote starts awaiting it. + // Keep rejection handled while preserving the original promise for wait(). + void this.result.catch(() => undefined); + } + + async listen(): Promise { + if (this.server) throw new Error("OAuth callback server is already running"); + this.server = createServer((request, response) => { + const url = new URL(request.url ?? "/", `http://127.0.0.1:${this.port}`); + if (request.method !== "GET" || url.pathname !== "/oauth/callback") { + response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("Not found"); + return; + } + const oauthError = url.searchParams.get("error"); + if (oauthError) { + response.writeHead(400, { "content-type": "text/plain; charset=utf-8" }).end("OAuth login failed. Return to the terminal."); + this.rejectResult?.(new Error(`OAuth authorization failed: ${oauthError}`)); + return; + } + if (!this.validateState(url.searchParams.get("state"))) { + response.writeHead(400, { "content-type": "text/plain; charset=utf-8" }).end("OAuth state did not match. Return to the terminal."); + this.rejectResult?.(new Error("OAuth callback state did not match")); + return; + } + const code = url.searchParams.get("code"); + if (!code) { + response.writeHead(400, { "content-type": "text/plain; charset=utf-8" }).end("OAuth authorization code is missing."); + this.rejectResult?.(new Error("OAuth callback did not include an authorization code")); + return; + } + response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end("CraftTable MCP

Login complete. You can close this window.

"); + this.resolveResult?.({ code }); + }); + await new Promise((resolve, reject) => { + const onError = (error: Error) => reject(new Error(`Could not listen on OAuth callback port ${this.port}: ${error.message}`)); + this.server!.once("error", onError); + this.server!.listen(this.port, "127.0.0.1", () => { + this.server!.off("error", onError); + resolve(); + }); + }); + const address = this.server.address() as AddressInfo | null; + if (!address || address.port !== this.port) throw new Error(`OAuth callback server did not bind port ${this.port}`); + } + + async wait(timeoutMs = 10 * 60 * 1000): Promise { + let timeout: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + this.result, + new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(new Error("OAuth callback timed out")), timeoutMs); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } + } + + async close(): Promise { + if (!this.server) return; + const server = this.server; + this.server = undefined; + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..d172ae3 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { Command } from "commander"; +import { configureAgents, type AgentTarget, unconfigureAgents } from "./agents.js"; +import { serveBridge } from "./bridge.js"; +import { type ClientOptions, credentialAccount, DiscoveryStore, resolveClientOptions } from "./config.js"; +import { KeyringTokenStore } from "./credentials.js"; +import { connectRemote, loginRemote, logoutRemote } from "./remote.js"; + +type ConnectionFlags = { url?: string; clientId?: string; callbackPort?: string }; + +const program = new Command() + .name("crafttable-mcp") + .description("OAuth client and stdio bridge for CraftTable MCP") + .version("0.1.0"); + +withConnection(program.command("login").description("Log in through the system browser")) + .option("--no-browser", "print the authorization URL instead of opening it") + .option("--timeout ", "OAuth callback timeout", "600000") + .action(async (flags: ConnectionFlags & { browser: boolean; timeout: string }) => { + const options = connectionOptions(flags); + const timeoutMs = positiveInteger(flags.timeout, "OAuth callback timeout"); + const result = await loginRemote(options, new KeyringTokenStore(), new DiscoveryStore(), { browser: flags.browser, timeoutMs }); + printJson({ loggedIn: true, alreadyAuthenticated: result.alreadyAuthenticated, server: options.url.toString(), tools: result.toolCount }); + }); + +withConnection(program.command("status").description("Check saved login state and remote MCP connectivity")) + .action(async (flags: ConnectionFlags) => { + const options = connectionOptions(flags); + const tokens = await new KeyringTokenStore().get(credentialAccount(options)); + const connection = await connectRemote(options, new KeyringTokenStore(), new DiscoveryStore()); + try { + const tools = await connection.client.listTools(); + printJson({ + loggedIn: Boolean(tokens), + authentication: connection.authentication, + connected: true, + server: options.url.toString(), + tools: tools.tools.length, + }); + } finally { + await connection.close(); + } + }); + +withConnection(program.command("logout").description("Revoke OAuth tokens and remove the local credential")) + .option("--local-only", "remove local credentials without contacting the authorization server") + .action(async (flags: ConnectionFlags & { localOnly?: boolean }) => { + const options = connectionOptions(flags); + const result = await logoutRemote(options, new KeyringTokenStore(), new DiscoveryStore(), Boolean(flags.localOnly)); + printJson({ loggedIn: false, credentialRemoved: result.hadCredential, revoked: result.revoked, localOnly: Boolean(flags.localOnly) }); + }); + +withConnection(program.command("tools").description("List remote MCP tools")) + .action(async (flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson(await client.listTools()))); + +withConnection(program.command("resources").description("List remote MCP resources and resource templates")) + .action(async (flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson({ + resources: (await client.listResources()).resources, + resourceTemplates: (await client.listResourceTemplates()).resourceTemplates, + }))); + +withConnection(program.command("read ").description("Read an MCP resource")) + .action(async (uri: string, flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson(await client.readResource({ uri })))); + +withConnection(program.command("call [json]").description("Call an MCP tool")) + .action(async (tool: string, json: string | undefined, flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => { + printJson(await client.callTool({ name: tool, arguments: parseObject(json ?? "{}") })); + })); + +withConnection(program.command("serve").description("Run the local stdio bridge")) + .action(async (flags: ConnectionFlags) => { + await serveBridge(connectionOptions(flags), new KeyringTokenStore(), new DiscoveryStore()); + }); + +for (const operation of ["configure", "unconfigure"] as const) { + withConnection(program.command(`${operation} `).description(`${operation === "configure" ? "Add" : "Remove"} the stdio bridge in Codex, Claude Code, or OpenCode`)) + .option("--dry-run", "show the planned changes without writing") + .option("--force", "replace a conflicting entry without prompting") + .action(async (agent: AgentTarget, flags: ConnectionFlags & { dryRun?: boolean; force?: boolean }) => { + const cliEntry = fileURLToPath(import.meta.url); + if (!cliEntry.endsWith(".js")) throw new Error("Agent configuration requires the built CLI; run `npm --prefix apps/mcp-client run build` first"); + const input = { + target: agent, + options: connectionOptions(flags), + cliEntry, + dryRun: Boolean(flags.dryRun), + force: Boolean(flags.force), + }; + const result = operation === "configure" ? await configureAgents(input) : await unconfigureAgents(input); + printJson({ result }); + }); +} + +function withConnection(command: Command): Command { + return command + .option("--url ", "MCP Streamable HTTP URL") + .option("--client-id ", "pre-registered OAuth public client ID") + .option("--callback-port ", "fixed localhost OAuth callback port"); +} + +function connectionOptions(flags: ConnectionFlags): ClientOptions { + return resolveClientOptions(flags); +} + +async function withClient(options: ClientOptions, action: (client: Awaited>["client"]) => Promise): Promise { + const connection = await connectRemote(options, new KeyringTokenStore(), new DiscoveryStore()); + try { + await action(connection.client); + } finally { + await connection.close(); + } +} + +function parseObject(raw: string): Record { + let value: unknown; + try { + value = JSON.parse(raw); + } catch { + throw new Error("Tool arguments must be a JSON object"); + } + if (!value || Array.isArray(value) || typeof value !== "object") throw new Error("Tool arguments must be a JSON object"); + return value as Record; +} + +function positiveInteger(value: string, name: string): number { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`); + return parsed; +} + +function printJson(value: unknown): void { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); +} + +program.parseAsync().catch((error: unknown) => { + process.stderr.write(`CraftTable MCP failed: ${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..fbbee2a --- /dev/null +++ b/src/config.ts @@ -0,0 +1,112 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type { OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js"; + +export const DEFAULT_MCP_URL = "https://crafttable.crash.work/mcp"; +export const DEFAULT_CLIENT_ID = "crafttable-mcp-cli"; +export const DEFAULT_CALLBACK_PORT = 48321; +export const OAUTH_SCOPES = "openid profile email offline_access"; + +export type ClientOptions = { + url: URL; + clientId: string; + callbackPort: number; +}; + +export function resolveClientOptions(input: { + url?: string; + clientId?: string; + callbackPort?: string | number; + env?: NodeJS.ProcessEnv; +} = {}): ClientOptions { + const env = input.env ?? process.env; + const url = validateMcpUrl(input.url ?? env.CRAFTTABLE_MCP_URL ?? DEFAULT_MCP_URL); + const clientId = (input.clientId ?? env.CRAFTTABLE_MCP_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID).trim(); + if (!clientId) throw new Error("OAuth client ID must not be empty"); + const callbackPort = Number(input.callbackPort ?? env.CRAFTTABLE_MCP_OAUTH_CALLBACK_PORT ?? DEFAULT_CALLBACK_PORT); + if (!Number.isInteger(callbackPort) || callbackPort < 1 || callbackPort > 65535) { + throw new Error("OAuth callback port must be an integer between 1 and 65535"); + } + return { url, clientId, callbackPort }; +} + +export function validateMcpUrl(value: string): URL { + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error("MCP URL must be an absolute URL"); + } + if (url.username || url.password || url.search || url.hash) { + throw new Error("MCP URL must not include credentials, query parameters, or a fragment"); + } + const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]"; + if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) { + throw new Error("MCP URL must use HTTPS unless it targets localhost"); + } + url.pathname = url.pathname.replace(/\/+$/, "") || "/"; + return url; +} + +export function credentialAccount(options: Pick): string { + return createHash("sha256").update(`${options.url.toString()}\0${options.clientId}`).digest("hex"); +} + +export function platformConfigDir(env: NodeJS.ProcessEnv = process.env, platform = process.platform): string { + if (platform === "win32") return path.join(env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "GameCraftTable", "mcp-client"); + if (platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "GameCraftTable", "mcp-client"); + return path.join(env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "gamecrafttable", "mcp-client"); +} + +type DiscoveryFile = { + version: 1; + entries: Record; +}; + +export class DiscoveryStore { + readonly filePath: string; + + constructor(configDir = platformConfigDir()) { + this.filePath = path.join(configDir, "discovery.json"); + } + + async get(account: string): Promise { + return (await this.read()).entries[account]; + } + + async set(account: string, state: OAuthDiscoveryState): Promise { + const document = await this.read(); + document.entries[account] = state; + await atomicWriteJson(this.filePath, document); + } + + async delete(account: string): Promise { + const document = await this.read(); + if (!(account in document.entries)) return; + delete document.entries[account]; + await atomicWriteJson(this.filePath, document); + } + + private async read(): Promise { + try { + const value = JSON.parse(await readFile(this.filePath, "utf8")) as Partial; + return { version: 1, entries: value.entries && typeof value.entries === "object" ? value.entries : {} }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 1, entries: {} }; + throw new Error(`Could not read OAuth discovery cache: ${errorMessage(error)}`); + } + } +} + +export async function atomicWriteJson(filePath: string, value: unknown): Promise { + await mkdir(path.dirname(filePath), { recursive: true }); + const temporary = `${filePath}.${process.pid}.tmp`; + await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); + await rename(temporary, filePath); +} + +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/src/credentials.ts b/src/credentials.ts new file mode 100644 index 0000000..038382b --- /dev/null +++ b/src/credentials.ts @@ -0,0 +1,64 @@ +import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; + +export const KEYRING_SERVICE = "CraftTable MCP"; + +export interface TokenStore { + get(account: string): Promise; + set(account: string, tokens: OAuthTokens): Promise; + delete(account: string): Promise; +} + +export class KeyringTokenStore implements TokenStore { + async get(account: string): Promise { + let serialized: string | undefined; + try { + const entry = await keyringEntry(account); + serialized = await entry.getPassword(); + } catch (error) { + throw keyringError(error); + } + if (!serialized) return undefined; + try { + const value = JSON.parse(serialized) as OAuthTokens; + if (!value.access_token || !value.token_type) throw new Error("missing token fields"); + return value; + } catch { + throw new Error("The saved CraftTable MCP credential is invalid; run `crafttable-mcp logout --local-only` and log in again"); + } + } + + async set(account: string, tokens: OAuthTokens): Promise { + try { + const entry = await keyringEntry(account); + await entry.setPassword(JSON.stringify(tokens)); + } catch (error) { + throw keyringError(error); + } + } + + async delete(account: string): Promise { + try { + const entry = await keyringEntry(account); + await entry.deleteCredential(); + } catch (error) { + const message = String((error as Error)?.message ?? error).toLowerCase(); + if (message.includes("no entry") || message.includes("not found")) return; + throw keyringError(error); + } + } +} + +async function keyringEntry(account: string): Promise { + try { + const { AsyncEntry } = await import("@napi-rs/keyring"); + return new AsyncEntry(KEYRING_SERVICE, account); + } catch (error) { + throw keyringError(error); + } +} + +function keyringError(error: unknown): Error { + if (error instanceof Error && error.message.startsWith("The operating-system credential store is unavailable")) return error; + const detail = error instanceof Error ? error.message : String(error); + return new Error(`The operating-system credential store is unavailable (${detail}). Enable Windows Credential Manager, macOS Keychain, or a Secret Service provider, then retry; plaintext token storage is not supported.`); +} diff --git a/src/oauthProvider.ts b/src/oauthProvider.ts new file mode 100644 index 0000000..684e249 --- /dev/null +++ b/src/oauthProvider.ts @@ -0,0 +1,84 @@ +import { randomBytes, timingSafeEqual } from "node:crypto"; +import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js"; +import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; +import type { ClientOptions } from "./config.js"; +import { credentialAccount, DiscoveryStore, OAUTH_SCOPES } from "./config.js"; +import type { TokenStore } from "./credentials.js"; + +export class GameCraftOAuthProvider implements OAuthClientProvider { + readonly redirectUrl: URL; + readonly clientMetadata: OAuthClientMetadata; + private readonly account: string; + private readonly expectedState: string; + private codeVerifierValue?: string; + + constructor( + private readonly options: ClientOptions, + private readonly tokenStore: TokenStore, + private readonly discoveryStore: DiscoveryStore, + private readonly onRedirect: (url: URL) => void | Promise, + state = randomBytes(32).toString("base64url"), + ) { + this.redirectUrl = new URL(`http://127.0.0.1:${options.callbackPort}/oauth/callback`); + this.clientMetadata = { + client_name: "CraftTable MCP CLI", + redirect_uris: [this.redirectUrl.toString()], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + token_endpoint_auth_method: "none", + scope: OAUTH_SCOPES, + }; + this.account = credentialAccount(options); + this.expectedState = state; + } + + state(): string { + return this.expectedState; + } + + validateState(value: string | null): boolean { + if (!value) return false; + const expected = Buffer.from(this.expectedState); + const actual = Buffer.from(value); + return expected.length === actual.length && timingSafeEqual(expected, actual); + } + + clientInformation(): OAuthClientInformationMixed { + return { client_id: this.options.clientId }; + } + + tokens(): Promise { + return this.tokenStore.get(this.account); + } + + saveTokens(tokens: OAuthTokens): Promise { + return this.tokenStore.set(this.account, tokens); + } + + redirectToAuthorization(url: URL): void | Promise { + return this.onRedirect(url); + } + + saveCodeVerifier(codeVerifier: string): void { + this.codeVerifierValue = codeVerifier; + } + + codeVerifier(): string { + if (!this.codeVerifierValue) throw new Error("OAuth PKCE verifier is missing or expired"); + return this.codeVerifierValue; + } + + discoveryState(): Promise { + return this.discoveryStore.get(this.account); + } + + saveDiscoveryState(state: OAuthDiscoveryState): Promise { + return this.discoveryStore.set(this.account, state); + } + + async invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise { + if (scope === "all" || scope === "tokens") await this.tokenStore.delete(this.account); + if (scope === "all" || scope === "discovery") await this.discoveryStore.delete(this.account); + if (scope === "all" || scope === "verifier") this.codeVerifierValue = undefined; + } +} diff --git a/src/remote.ts b/src/remote.ts new file mode 100644 index 0000000..431e15d --- /dev/null +++ b/src/remote.ts @@ -0,0 +1,167 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; +import open from "open"; +import { OAuthCallbackServer } from "./callbackServer.js"; +import type { ClientOptions } from "./config.js"; +import { credentialAccount, DiscoveryStore, errorMessage } from "./config.js"; +import type { TokenStore } from "./credentials.js"; +import { GameCraftOAuthProvider } from "./oauthProvider.js"; + +export type RemoteConnection = { + client: Client; + transport: StreamableHTTPClientTransport; + authentication: "oauth" | "service-token"; + close: () => Promise; +}; + +export async function connectRemote( + options: ClientOptions, + tokenStore: TokenStore, + discoveryStore: DiscoveryStore, + serviceToken = process.env.CRAFTTABLE_MCP_TOKEN?.trim(), +): Promise { + const tokens = await tokenStore.get(credentialAccount(options)); + if (tokens) { + const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, () => { + throw new Error("OAuth login is required; run `crafttable-mcp login`"); + }); + return connectWithTransport(options, new StreamableHTTPClientTransport(options.url, { authProvider: provider }), "oauth"); + } + if (serviceToken) { + return connectWithTransport(options, new StreamableHTTPClientTransport(options.url, { + requestInit: { headers: { authorization: `Bearer ${serviceToken}` } }, + }), "service-token"); + } + throw new Error("Not logged in; run `crafttable-mcp login` or set CRAFTTABLE_MCP_TOKEN for the legacy service-account path"); +} + +export async function loginRemote( + options: ClientOptions, + tokenStore: TokenStore, + discoveryStore: DiscoveryStore, + input: { browser: boolean; timeoutMs?: number; writeLine?: (value: string) => void } = { browser: true }, +): Promise<{ alreadyAuthenticated: boolean; toolCount: number }> { + const writeLine = input.writeLine ?? ((value) => process.stderr.write(`${value}\n`)); + let authorizationUrl: URL | undefined; + const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, async (url) => { + authorizationUrl = url; + if (!input.browser) { + writeLine(`Open this URL to log in:\n${url.toString()}`); + return; + } + try { + await open(url.toString(), { wait: false }); + writeLine("Opened the system browser for Game-CraftTable login."); + } catch (error) { + writeLine(`Could not open the browser (${errorMessage(error)}). Open this URL manually:\n${url.toString()}`); + } + }); + const callback = new OAuthCallbackServer(options.callbackPort, (state) => provider.validateState(state)); + await callback.listen(); + const firstClient = new Client({ name: "crafttable-mcp-cli", version: "0.1.0" }); + const firstTransport = new StreamableHTTPClientTransport(options.url, { authProvider: provider }); + try { + try { + await firstClient.connect(firstTransport); + const tools = await firstClient.listTools(); + return { alreadyAuthenticated: true, toolCount: tools.tools.length }; + } catch (error) { + if (!(error instanceof UnauthorizedError) && !authorizationUrl) throw error; + const { code } = await callback.wait(input.timeoutMs); + await firstTransport.finishAuth(code); + } + } finally { + await firstClient.close().catch(() => undefined); + await callback.close().catch(() => undefined); + } + + const connection = await connectRemote(options, tokenStore, discoveryStore, ""); + try { + const tools = await connection.client.listTools(); + return { alreadyAuthenticated: false, toolCount: tools.tools.length }; + } finally { + await connection.close(); + } +} + +export async function logoutRemote( + options: ClientOptions, + tokenStore: TokenStore, + discoveryStore: DiscoveryStore, + localOnly: boolean, + fetchFn: typeof fetch = fetch, +): Promise<{ hadCredential: boolean; revoked: boolean }> { + const account = credentialAccount(options); + const tokens = await tokenStore.get(account); + if (tokens && !localOnly) { + const endpoint = await discoverRevocationEndpoint(options.url, fetchFn); + await revokeTokens(endpoint, options.clientId, tokens, fetchFn); + } + await tokenStore.delete(account); + await discoveryStore.delete(account); + return { hadCredential: Boolean(tokens), revoked: Boolean(tokens && !localOnly) }; +} + +async function connectWithTransport( + options: ClientOptions, + transport: StreamableHTTPClientTransport, + authentication: RemoteConnection["authentication"], +): Promise { + const client = new Client({ name: "crafttable-mcp-cli", version: "0.1.0" }); + try { + await client.connect(transport); + } catch (error) { + await client.close().catch(() => undefined); + if (error instanceof UnauthorizedError) throw new Error("OAuth login is required; run `crafttable-mcp login`"); + throw error; + } + return { + client, + transport, + authentication, + close: () => client.close(), + }; +} + +async function discoverRevocationEndpoint(resource: URL, fetchFn: typeof fetch): Promise { + const metadataUrl = new URL(`/.well-known/oauth-protected-resource${resource.pathname === "/" ? "" : resource.pathname}`, resource.origin); + const protectedResponse = await fetchFn(metadataUrl, { headers: { accept: "application/json" } }); + if (!protectedResponse.ok) throw new Error(`OAuth protected-resource discovery returned HTTP ${protectedResponse.status}`); + const protectedMetadata = await protectedResponse.json() as { authorization_servers?: unknown }; + const issuer = Array.isArray(protectedMetadata.authorization_servers) + ? protectedMetadata.authorization_servers.find((value): value is string => typeof value === "string") + : undefined; + if (!issuer) throw new Error("OAuth protected-resource metadata has no authorization server"); + const issuerUrl = new URL(issuer); + const candidates = [ + new URL(`${issuerUrl.toString().replace(/\/$/, "")}/.well-known/openid-configuration`), + new URL(`/.well-known/openid-configuration${issuerUrl.pathname === "/" ? "" : issuerUrl.pathname}`, issuerUrl.origin), + ]; + for (const candidate of candidates) { + const response = await fetchFn(candidate, { headers: { accept: "application/json" } }); + if (!response.ok) continue; + try { + const metadata = await response.json() as { revocation_endpoint?: unknown }; + if (typeof metadata.revocation_endpoint === "string") return new URL(metadata.revocation_endpoint); + } catch { + // Try the next standards-compatible discovery location. + } + } + throw new Error("OAuth authorization server does not advertise a revocation endpoint"); +} + +async function revokeTokens(endpoint: URL, clientId: string, tokens: OAuthTokens, fetchFn: typeof fetch): Promise { + const candidates = [tokens.refresh_token + ? { token: tokens.refresh_token, hint: "refresh_token" } + : { token: tokens.access_token, hint: "access_token" }]; + for (const value of candidates) { + const response = await fetchFn(endpoint, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, + body: new URLSearchParams({ token: value.token, token_type_hint: value.hint, client_id: clientId }), + }); + if (!response.ok) throw new Error(`OAuth token revocation returned HTTP ${response.status}; local credentials were retained`); + } +} diff --git a/test/agents.test.ts b/test/agents.test.ts new file mode 100644 index 0000000..6c07da5 --- /dev/null +++ b/test/agents.test.ts @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { parse } from "jsonc-parser"; +import { configureAgents, type CommandResult, type CommandRunner, launchCommand, unconfigureAgents } from "../src/agents.js"; +import { resolveClientOptions } from "../src/config.js"; + +class FakeRunner implements CommandRunner { + readonly calls: Array<{ command: string; args: string[] }> = []; + existing: Record = {}; + + async run(command: string, args: string[]): Promise { + this.calls.push({ command, args }); + const agent = command === "codex" ? "codex" : "claude"; + if (args[1] === "get") { + const value = this.existing[agent]; + return value === undefined + ? { code: 1, stdout: "", stderr: "MCP server not found" } + : { code: 0, stdout: value, stderr: "" }; + } + if (args[1] === "remove") { + this.existing[agent] = undefined; + return { code: 0, stdout: "", stderr: "" }; + } + if (args[1] === "add") { + const separator = args.indexOf("--"); + const commandParts = args.slice(separator + 1); + this.existing[agent] = JSON.stringify({ transport: { command: commandParts[0], args: commandParts.slice(1) } }); + return { code: 0, stdout: "", stderr: "" }; + } + return { code: 1, stdout: "", stderr: "unexpected command" }; + } +} + +const options = resolveClientOptions({ url: "https://example.test/mcp", clientId: "client", callbackPort: 48321, env: {} }); +const cliEntry = path.resolve("dist", "cli.js"); +const nodePath = path.resolve("bin", "node.exe"); + +test("Codex and Claude adapters add, detect idempotency, replace conflicts, remove, and dry-run", async () => { + const runner = new FakeRunner(); + assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "added" }]); + assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "unchanged" }]); + runner.existing.codex = JSON.stringify({ transport: { command: "other", args: [] } }); + await assert.rejects(configureAgents({ target: "codex", options, cliEntry, nodePath, runner, confirm: async () => false }), /not changed/); + assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner, force: true }), [{ agent: "codex", action: "replaced" }]); + assert.deepEqual(await unconfigureAgents({ target: "codex", options, cliEntry, nodePath, runner, dryRun: true }), [{ agent: "codex", action: "would-remove" }]); + assert.deepEqual(await unconfigureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "removed" }]); + + assert.deepEqual(await configureAgents({ target: "claude", options, cliEntry, nodePath, runner, dryRun: true }), [{ agent: "claude", action: "would-add" }]); + assert.equal(runner.existing.claude, undefined); +}); + +test("OpenCode adapter preserves JSONC, creates one backup, is idempotent, and unconfigures", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "crafttable-mcp-agent-test-")); + const filePath = path.join(directory, "opencode.json"); + await import("node:fs/promises").then(({ writeFile }) => writeFile(filePath, "{\n // keep this comment\n \"theme\": \"dark\"\n}\n", "utf8")); + try { + assert.deepEqual(await configureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "added" }]); + const configuredText = await readFile(filePath, "utf8"); + assert.match(configuredText, /keep this comment/); + const configured = parse(configuredText) as { theme: string; mcp: Record }; + assert.equal(configured.theme, "dark"); + assert.equal(configured.mcp.crafttable?.type, "local"); + assert.deepEqual(configured.mcp.crafttable?.command, launchCommand(options, cliEntry, nodePath)); + assert.match(await readFile(`${filePath}.crafttable-mcp.backup`, "utf8"), /keep this comment/); + assert.deepEqual(await configureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "unchanged" }]); + assert.deepEqual(await unconfigureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "removed" }]); + const removed = parse(await readFile(filePath, "utf8")) as { mcp?: Record }; + assert.equal(removed.mcp?.crafttable, undefined); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/test/bridge.test.ts b/test/bridge.test.ts new file mode 100644 index 0000000..62cfa84 --- /dev/null +++ b/test/bridge.test.ts @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createProxyServer } from "../src/bridge.js"; + +test("stdio proxy forwards tools, resources, templates, reads, errors, and cursors", async () => { + const calls: Array<{ method: string; params: unknown }> = []; + const upstream = { + listTools: async (params?: { cursor?: string }) => { + calls.push({ method: "tools/list", params }); + return { tools: [{ name: "list_spaces", description: "List spaces", inputSchema: { type: "object" } }], nextCursor: "tools-next" }; + }, + callTool: async (params: { name: string; arguments?: Record }) => { + calls.push({ method: "tools/call", params }); + if (params.name === "explode") throw new Error("upstream failed"); + return { content: [{ type: "text", text: JSON.stringify(params.arguments) }] }; + }, + listResources: async (params?: { cursor?: string }) => { + calls.push({ method: "resources/list", params }); + return { resources: [{ uri: "gamecraft://spaces", name: "Spaces" }], nextCursor: "resources-next" }; + }, + listResourceTemplates: async (params?: { cursor?: string }) => { + calls.push({ method: "resources/templates/list", params }); + return { resourceTemplates: [{ uriTemplate: "gamecraft://space/{spaceId}", name: "Space" }] }; + }, + readResource: async (params: { uri: string }) => { + calls.push({ method: "resources/read", params }); + return { contents: [{ uri: params.uri, text: "resource-value" }] }; + }, + }; + const server = createProxyServer(upstream); + const client = new Client({ name: "bridge-test", version: "1.0.0" }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + assert.equal((await client.listTools({ cursor: "tool-cursor" })).nextCursor, "tools-next"); + assert.equal((await client.listResources({ cursor: "resource-cursor" })).nextCursor, "resources-next"); + assert.equal((await client.listResourceTemplates({ cursor: "template-cursor" })).resourceTemplates.length, 1); + const resource = await client.readResource({ uri: "gamecraft://spaces" }); + assert.equal("text" in resource.contents[0]! ? resource.contents[0].text : undefined, "resource-value"); + const called = await client.callTool({ name: "list_spaces", arguments: { page: 2 } }); + const content = called.content as Array<{ type: string; text?: string }>; + assert.match(String(content[0]?.type === "text" ? content[0].text : ""), /\"page\":2/); + await assert.rejects(client.callTool({ name: "explode", arguments: {} }), /upstream failed/); + assert.deepEqual(calls.slice(0, 3), [ + { method: "tools/list", params: { cursor: "tool-cursor" } }, + { method: "resources/list", params: { cursor: "resource-cursor" } }, + { method: "resources/templates/list", params: { cursor: "template-cursor" } }, + ]); + } finally { + await client.close(); + await server.close(); + } +}); diff --git a/test/configAndOAuth.test.ts b/test/configAndOAuth.test.ts new file mode 100644 index 0000000..78d086e --- /dev/null +++ b/test/configAndOAuth.test.ts @@ -0,0 +1,128 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:net"; +import test from "node:test"; +import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { OAuthCallbackServer } from "../src/callbackServer.js"; +import { credentialAccount, resolveClientOptions, validateMcpUrl } from "../src/config.js"; +import type { TokenStore } from "../src/credentials.js"; +import { GameCraftOAuthProvider } from "../src/oauthProvider.js"; +import { logoutRemote } from "../src/remote.js"; + +class MemoryTokens implements TokenStore { + readonly values = new Map(); + async get(account: string): Promise { return this.values.get(account); } + async set(account: string, tokens: OAuthTokens): Promise { this.values.set(account, tokens); } + async delete(account: string): Promise { this.values.delete(account); } +} + +class MemoryDiscovery { + values = new Map(); + async get(account: string): Promise { return this.values.get(account) as undefined; } + async set(account: string, state: unknown): Promise { this.values.set(account, state); } + async delete(account: string): Promise { this.values.delete(account); } +} + +test("connection options enforce HTTPS and isolate credentials by URL and client ID", () => { + assert.equal(validateMcpUrl("https://example.test/mcp/").toString(), "https://example.test/mcp"); + assert.equal(validateMcpUrl("http://127.0.0.1:8787/mcp").protocol, "http:"); + assert.throws(() => validateMcpUrl("http://example.test/mcp"), /HTTPS/); + assert.throws(() => validateMcpUrl("https://example.test/mcp?token=secret"), /query/); + const first = resolveClientOptions({ url: "https://one.test/mcp", clientId: "client", callbackPort: 48321, env: {} }); + const second = resolveClientOptions({ url: "https://two.test/mcp", clientId: "client", callbackPort: 48321, env: {} }); + const third = resolveClientOptions({ url: "https://one.test/mcp", clientId: "other", callbackPort: 48321, env: {} }); + assert.notEqual(credentialAccount(first), credentialAccount(second)); + assert.notEqual(credentialAccount(first), credentialAccount(third)); +}); + +test("OAuth provider validates state and replaces rotated refresh tokens", async () => { + const options = resolveClientOptions({ url: "https://example.test/mcp", clientId: "client", callbackPort: 48321, env: {} }); + const tokens = new MemoryTokens(); + const discovery = new MemoryDiscovery(); + const provider = new GameCraftOAuthProvider(options, tokens, discovery as never, () => undefined, "expected-state"); + assert.equal(provider.validateState("expected-state"), true); + assert.equal(provider.validateState("wrong-state"), false); + provider.saveCodeVerifier("verifier"); + assert.equal(provider.codeVerifier(), "verifier"); + await provider.saveTokens({ access_token: "access-one", refresh_token: "refresh-one", token_type: "Bearer" }); + await provider.saveTokens({ access_token: "access-two", refresh_token: "refresh-two", token_type: "Bearer" }); + assert.equal((await provider.tokens())?.refresh_token, "refresh-two"); +}); + +test("OAuth callback validates state without exposing the authorization code", async () => { + const port = await availablePort(); + const callback = new OAuthCallbackServer(port, (state) => state === "expected"); + await callback.listen(); + try { + const response = await fetch(`http://127.0.0.1:${port}/oauth/callback?code=sensitive-code&state=expected`); + assert.equal(response.status, 200); + assert.doesNotMatch(await response.text(), /sensitive-code/); + assert.deepEqual(await callback.wait(1000), { code: "sensitive-code" }); + } finally { + await callback.close(); + } +}); + +test("OAuth callback reports timeout and occupied ports", async () => { + const timeoutPort = await availablePort(); + const timeoutServer = new OAuthCallbackServer(timeoutPort, () => true); + await timeoutServer.listen(); + try { + await assert.rejects(timeoutServer.wait(10), /timed out/); + } finally { + await timeoutServer.close(); + } + + const occupiedPort = await availablePort(); + const blocker = createServer(); + await new Promise((resolve) => blocker.listen(occupiedPort, "127.0.0.1", resolve)); + try { + await assert.rejects(new OAuthCallbackServer(occupiedPort, () => true).listen(), /Could not listen/); + } finally { + await new Promise((resolve, reject) => blocker.close((error) => error ? reject(error) : resolve())); + } +}); + +test("logout revokes refresh and access tokens before deleting local credentials", async () => { + const options = resolveClientOptions({ url: "https://example.test/mcp", clientId: "client", callbackPort: 48321, env: {} }); + const account = credentialAccount(options); + const tokens = new MemoryTokens(); + tokens.values.set(account, { access_token: "access", refresh_token: "refresh", token_type: "Bearer" }); + const discovery = new MemoryDiscovery(); + const requests: Array<{ url: string; body?: string }> = []; + const fetchFn = async (input: string | URL | Request, init?: RequestInit): Promise => { + const url = String(input); + requests.push({ url, body: init?.body?.toString() }); + if (url.includes("oauth-protected-resource")) return Response.json({ authorization_servers: ["https://id.example/oidc"] }); + if (url.includes("openid-configuration")) return Response.json({ revocation_endpoint: "https://id.example/revoke" }); + return new Response(null, { status: 200 }); + }; + const result = await logoutRemote(options, tokens, discovery as never, false, fetchFn as typeof fetch); + assert.deepEqual(result, { hadCredential: true, revoked: true }); + assert.equal(tokens.values.has(account), false); + assert.equal(requests.filter((request) => request.url.endsWith("/revoke")).length, 1); + assert.match(requests.find((request) => request.body?.includes("refresh_token"))?.body ?? "", /token=refresh/); +}); + +test("failed remote revocation retains local credentials", async () => { + const options = resolveClientOptions({ url: "https://example.test/mcp", clientId: "client", callbackPort: 48321, env: {} }); + const account = credentialAccount(options); + const tokens = new MemoryTokens(); + tokens.values.set(account, { access_token: "access", token_type: "Bearer" }); + const fetchFn = async (input: string | URL | Request): Promise => { + const url = String(input); + if (url.includes("oauth-protected-resource")) return Response.json({ authorization_servers: ["https://id.example/oidc"] }); + if (url.includes("openid-configuration")) return Response.json({ revocation_endpoint: "https://id.example/revoke" }); + return new Response(null, { status: 500 }); + }; + await assert.rejects(logoutRemote(options, tokens, new MemoryDiscovery() as never, false, fetchFn as typeof fetch), /retained/); + assert.equal(tokens.values.has(account), true); +}); + +async function availablePort(): Promise { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("could not allocate test port"); + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + return address.port; +} diff --git a/test/oauthIntegration.test.ts b/test/oauthIntegration.test.ts new file mode 100644 index 0000000..bef9c95 --- /dev/null +++ b/test/oauthIntegration.test.ts @@ -0,0 +1,159 @@ +import assert from "node:assert/strict"; +import { createHash, randomUUID } from "node:crypto"; +import express from "express"; +import type { AddressInfo } from "node:net"; +import test from "node:test"; +import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { credentialAccount, resolveClientOptions } from "../src/config.js"; +import type { TokenStore } from "../src/credentials.js"; +import { loginRemote } from "../src/remote.js"; + +class MemoryTokens implements TokenStore { + readonly values = new Map(); + async get(account: string): Promise { return this.values.get(account); } + async set(account: string, tokens: OAuthTokens): Promise { this.values.set(account, tokens); } + async delete(account: string): Promise { this.values.delete(account); } +} + +class MemoryDiscovery { + value: unknown; + async get(): Promise { return this.value as undefined; } + async set(_account: string, value: unknown): Promise { this.value = value; } + async delete(): Promise { this.value = undefined; } +} + +test("browser PKCE login exchanges, refreshes rotated tokens, and initializes MCP without leaking secrets", async () => { + const callbackPort = await availablePort(); + const app = express(); + app.use(express.urlencoded({ extended: false })); + app.use(express.json()); + const server = app.listen(0, "127.0.0.1"); + await new Promise((resolve) => server.once("listening", resolve)); + const address = server.address() as AddressInfo; + const origin = `http://127.0.0.1:${address.port}`; + const resource = `${origin}/mcp`; + const authorizationCodes = new Map(); + let authorizationRequest: URL | undefined; + let refreshCount = 0; + + let mcpServer: Server | undefined; + let transport: StreamableHTTPServerTransport | undefined; + + app.get("/.well-known/oauth-protected-resource/mcp", (_request, response) => response.json({ + resource, + authorization_servers: [origin], + scopes_supported: ["openid", "profile", "email", "offline_access"], + bearer_methods_supported: ["header"], + })); + const authorizationMetadata = { + issuer: origin, + authorization_endpoint: `${origin}/authorize`, + token_endpoint: `${origin}/token`, + revocation_endpoint: `${origin}/revoke`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + code_challenge_methods_supported: ["S256"], + token_endpoint_auth_methods_supported: ["none"], + }; + app.get("/.well-known/oauth-authorization-server", (_request, response) => response.json(authorizationMetadata)); + app.get("/.well-known/openid-configuration", (_request, response) => response.json({ + ...authorizationMetadata, + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["RS256"], + })); + app.get("/authorize", (request, response) => { + authorizationRequest = new URL(request.originalUrl, origin); + const redirectUri = String(request.query.redirect_uri ?? ""); + const state = String(request.query.state ?? ""); + const challenge = String(request.query.code_challenge ?? ""); + const requestedResource = String(request.query.resource ?? ""); + const code = "authorization-code"; + authorizationCodes.set(code, { challenge, resource: requestedResource }); + const callback = new URL(redirectUri); + callback.searchParams.set("code", code); + callback.searchParams.set("state", state); + response.redirect(callback.toString()); + }); + app.post("/token", (request, response) => { + const grantType = String(request.body.grant_type ?? ""); + if (grantType === "authorization_code") { + const code = String(request.body.code ?? ""); + const record = authorizationCodes.get(code); + const verifier = String(request.body.code_verifier ?? ""); + const challenge = createHash("sha256").update(verifier).digest("base64url"); + if (!record || challenge !== record.challenge || record.resource !== resource || request.body.resource !== resource || request.body.client_id !== "test-client") { + response.status(400).json({ error: "invalid_grant" }); + return; + } + response.json({ access_token: "access-one", refresh_token: "refresh-one", token_type: "Bearer", expires_in: 3600 }); + return; + } + if (grantType === "refresh_token" && request.body.refresh_token === "refresh-one" && request.body.resource === resource) { + refreshCount += 1; + response.json({ access_token: "access-two", refresh_token: "refresh-two", token_type: "Bearer", expires_in: 3600 }); + return; + } + response.status(400).json({ error: "invalid_grant" }); + }); + app.post("/mcp", async (request, response) => { + if (request.headers.authorization !== "Bearer access-two") { + response.setHeader("WWW-Authenticate", `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource/mcp"`); + response.status(401).json({ error: "authentication required" }); + return; + } + if (!transport && isInitializeRequest(request.body)) { + mcpServer = new Server({ name: "mock-gamecraft", version: "1.0.0" }, { capabilities: { tools: {} } }); + mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [{ name: "list_spaces", description: "List spaces", inputSchema: { type: "object" } }], + })); + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true }); + await mcpServer.connect(transport); + } + if (!transport) { + response.status(400).json({ error: "MCP session is missing" }); + return; + } + await transport.handleRequest(request, response, request.body); + }); + + const options = resolveClientOptions({ url: resource, clientId: "test-client", callbackPort, env: {} }); + const tokens = new MemoryTokens(); + const discovery = new MemoryDiscovery(); + const output: string[] = []; + let browserFailure: unknown; + try { + const result = await loginRemote(options, tokens, discovery as never, { + browser: false, + timeoutMs: 3000, + writeLine: (line) => { + output.push(line); + const match = line.match(/https?:\/\/[^\s]+/); + if (match) void fetch(match[0], { redirect: "follow" }).catch((error) => { browserFailure = error; }); + }, + }); + assert.equal(browserFailure, undefined); + assert.deepEqual(result, { alreadyAuthenticated: false, toolCount: 1 }); + assert.equal(authorizationRequest?.searchParams.get("code_challenge_method"), "S256"); + assert.equal(authorizationRequest?.searchParams.get("resource"), resource); + assert.match(authorizationRequest?.searchParams.get("scope") ?? "", /offline_access/); + assert.equal(refreshCount, 1); + assert.equal(tokens.values.get(credentialAccount(options))?.refresh_token, "refresh-two"); + const renderedOutput = output.join("\n"); + assert.doesNotMatch(renderedOutput, /authorization-code|access-one|access-two|refresh-one|refresh-two|Authorization:/); + } finally { + await mcpServer?.close(); + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + } +}); + +async function availablePort(): Promise { + const app = express(); + const server = app.listen(0, "127.0.0.1"); + await new Promise((resolve) => server.once("listening", resolve)); + const address = server.address() as AddressInfo; + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + return address.port; +} diff --git a/test/stdioFixture.ts b/test/stdioFixture.ts new file mode 100644 index 0000000..3817c66 --- /dev/null +++ b/test/stdioFixture.ts @@ -0,0 +1,12 @@ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createProxyServer } from "../src/bridge.js"; + +const server = createProxyServer({ + listTools: async (params) => ({ tools: [{ name: "fixture_tool", inputSchema: { type: "object" } }], nextCursor: params?.cursor }), + callTool: async () => ({ content: [{ type: "text", text: "ok" }] }), + listResources: async () => ({ resources: [] }), + listResourceTemplates: async () => ({ resourceTemplates: [] }), + readResource: async ({ uri }) => ({ contents: [{ uri, text: "ok" }] }), +}); + +await server.connect(new StdioServerTransport()); diff --git a/test/stdioProtocol.test.ts b/test/stdioProtocol.test.ts new file mode 100644 index 0000000..601efb3 --- /dev/null +++ b/test/stdioProtocol.test.ts @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +test("stdio protocol mode writes only JSON-RPC messages to stdout", async () => { + const packageRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url))); + const child = spawn(process.execPath, ["--import", "tsx", "test/stdioFixture.ts"], { + cwd: packageRoot, + shell: false, + windowsHide: true, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += String(chunk); }); + child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += String(chunk); }); + const messages = [ + { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "stdio-test", version: "1.0.0" } } }, + { jsonrpc: "2.0", method: "notifications/initialized" }, + { jsonrpc: "2.0", id: 2, method: "tools/list", params: { cursor: "next" } }, + ]; + child.stdin.write(messages.map((message) => JSON.stringify(message)).join("\n") + "\n"); + await waitFor(() => stdout.split(/\r?\n/).filter(Boolean).some((line) => { + try { return (JSON.parse(line) as { id?: number }).id === 2; } catch { return false; } + }), 3000); + child.stdin.end(); + await new Promise((resolve) => { + const timeout = setTimeout(() => { child.kill(); resolve(); }, 1000); + child.once("close", () => { clearTimeout(timeout); resolve(); }); + }); + const lines = stdout.split(/\r?\n/).filter(Boolean); + assert.ok(lines.length >= 2); + const parsed = lines.map((line) => JSON.parse(line) as { jsonrpc: string; id?: number; result?: unknown }); + assert.ok(parsed.every((message) => message.jsonrpc === "2.0")); + assert.ok(parsed.some((message) => message.id === 1)); + assert.ok(parsed.some((message) => message.id === 2)); + assert.equal(stderr, ""); +}); + +async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { + const started = Date.now(); + while (!predicate()) { + if (Date.now() - started > timeoutMs) throw new Error("timed out waiting for stdio response"); + await new Promise((resolve) => setTimeout(resolve, 10)); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8c11483 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node"], + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist" + }, + "include": ["src", "test"] +}