This server wants you to sign in before it hands over its tools.
+
+
+ )}
+
+ {mcp.error &&
{mcp.error}
}
+
+ {mcp.catalog.length > 0 && (
+
+ {mcp.catalog.map((t) => (
+
+ {t.name}
+ {t.description && {t.description}}
+
+ ))}
+
+ )}
+
+
+ Your browser talks to the server directly, so it must send CORS headers for this origin —
+ including Access-Control-Expose-Headers: WWW-Authenticate, mcp-session-id, or
+ the OAuth challenge can’t be read. Nothing is proxied through us; no server sees your token.
+
+
+ )
+}
diff --git a/package-lock.json b/package-lock.json
index 84eed1b..8117fdc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@dudko.dev/agent-web-react",
- "version": "0.0.3",
+ "version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@dudko.dev/agent-web-react",
- "version": "0.0.3",
+ "version": "0.0.4",
"funding": [
{
"type": "individual",
@@ -27,7 +27,8 @@
],
"license": "MIT",
"devDependencies": {
- "@dudko.dev/agent-web": "^0.0.7",
+ "@dudko.dev/agent-web": "^0.0.12",
+ "@modelcontextprotocol/sdk": "^1.30.0",
"@types/node": "^22.9.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
@@ -41,64 +42,66 @@
"node": ">=18"
},
"peerDependencies": {
- "@dudko.dev/agent-web": ">=0.0.6",
+ "@dudko.dev/agent-web": ">=0.0.12",
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/@ai-sdk/gateway": {
- "version": "3.0.142",
- "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.142.tgz",
- "integrity": "sha512-Y1iwdxdebYXpoK5y/4CrcCfJGeFwEJWlEx+pMWIg/ZVWGi9KA+JXM7YBkhxcshpq/jAXx8YyQeFMyZr0TGfXZQ==",
+ "version": "4.0.52",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.52.tgz",
+ "integrity": "sha512-SXUM8jzzuTUJRq+EOgPd5to6DSx0EKslVn+IVZHbUEX6k/3vCPNrvjckbK26HnNxHU/STxm+zTSJteqrO+7Z0w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider": "3.0.13",
- "@ai-sdk/provider-utils": "4.0.35",
+ "@ai-sdk/provider": "4.0.7",
+ "@ai-sdk/provider-utils": "5.0.27",
"@vercel/oidc": "3.2.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=22"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/provider": {
- "version": "3.0.13",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.13.tgz",
- "integrity": "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.7.tgz",
+ "integrity": "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=22"
}
},
"node_modules/@ai-sdk/provider-utils": {
- "version": "4.0.35",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.35.tgz",
- "integrity": "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg==",
+ "version": "5.0.27",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.27.tgz",
+ "integrity": "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider": "3.0.13",
+ "@ai-sdk/provider": "4.0.7",
"@standard-schema/spec": "^1.1.0",
- "eventsource-parser": "^3.0.8"
+ "@workflow/serde": "4.1.0",
+ "eventsource-parser": "^3.0.8",
+ "undici": "^7.28.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=22"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@dudko.dev/agent-web": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/@dudko.dev/agent-web/-/agent-web-0.0.7.tgz",
- "integrity": "sha512-iMtkVPpmC1A2sl/LRrY1u8viXS7WDr09TNNrvfPZsezmxa4lvapwChlKFzBa3cxRq4ioP8K2BBChKkNk4u22Rw==",
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@dudko.dev/agent-web/-/agent-web-0.0.12.tgz",
+ "integrity": "sha512-fD8jfemVjSxGdC4y3cftpGKCtIstN66jXcCWb1/mPiMxPCqK52H9BPGooKtAT11hNm2LHYmiJvtlfSNfLb95JA==",
"dev": true,
"funding": [
{
@@ -120,7 +123,7 @@
],
"license": "MIT",
"dependencies": {
- "ai": "^6.0.218",
+ "ai": "^7.0.66",
"idb": "^8.0.0",
"zod": "^4.4.3"
},
@@ -128,16 +131,16 @@
"node": ">=18"
},
"peerDependencies": {
- "@ai-sdk/anthropic": "^3.0.0",
- "@ai-sdk/deepseek": "^2.0.0",
- "@ai-sdk/google": "^3.0.0",
- "@ai-sdk/openai": "^3.0.0",
- "@ai-sdk/openai-compatible": "^2.0.0",
- "@ai-sdk/xai": "^3.0.0",
- "@browser-ai/core": "^2.0.0",
- "@browser-ai/web-llm": "^2.0.0",
+ "@ai-sdk/anthropic": "^4.0.0",
+ "@ai-sdk/deepseek": "^3.0.0",
+ "@ai-sdk/google": "^4.0.0",
+ "@ai-sdk/openai": "^4.0.0",
+ "@ai-sdk/openai-compatible": "^3.0.0",
+ "@ai-sdk/xai": "^4.0.0",
+ "@browser-ai/core": "^3.0.0",
+ "@browser-ai/web-llm": "^3.0.0",
"@mlc-ai/web-llm": ">=0.2.70",
- "@modelcontextprotocol/sdk": "^1.0.0"
+ "@modelcontextprotocol/sdk": "^1.30.0"
},
"peerDependenciesMeta": {
"@ai-sdk/anthropic": {
@@ -614,6 +617,19 @@
"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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -653,14 +669,45 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@opentelemetry/api": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
- "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "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==",
"dev": true,
- "license": "Apache-2.0",
+ "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": ">=8.0.0"
+ "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/@rollup/rollup-android-arm-eabi": {
@@ -1067,6 +1114,27 @@
"node": ">= 20"
}
},
+ "node_modules/@workflow/serde": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0.tgz",
+ "integrity": "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==",
+ "dev": true,
+ "license": "Apache-2.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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
@@ -1081,24 +1149,58 @@
}
},
"node_modules/ai": {
- "version": "6.0.218",
- "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.218.tgz",
- "integrity": "sha512-HsyCUNaaYgX/b/kGOoYfKkqfT1HvpUKKDb8YkN1FKeCNZjKdqXLGY+cKBpYGIRAvsPuOHskxLxZ46cK1dTBWQQ==",
+ "version": "7.0.66",
+ "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.66.tgz",
+ "integrity": "sha512-wBUyoCYF3GVr+62nelBgR8YbpTSsMZrzFyOOjiwijylNSM2TFCW35C+Pml2vc59/WLMpyhS/LWZ55M+B9DAcSg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/gateway": "3.0.142",
- "@ai-sdk/provider": "3.0.13",
- "@ai-sdk/provider-utils": "4.0.35",
- "@opentelemetry/api": "^1.9.0"
+ "@ai-sdk/gateway": "4.0.52",
+ "@ai-sdk/provider": "4.0.7",
+ "@ai-sdk/provider-utils": "5.0.27"
},
"engines": {
- "node": ">=18"
+ "node": ">=22"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -1106,6 +1208,45 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/bundle-require": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz",
@@ -1122,6 +1263,16 @@
"esbuild": ">=0.18"
}
},
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -1132,6 +1283,37 @@
"node": ">=8"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1175,6 +1357,83 @@
"node": "^14.18.0 || >=16.10.0"
}
},
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1200,6 +1459,81 @@
}
}
},
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
@@ -1242,6 +1576,36 @@
"@esbuild/win32-x64": "0.27.7"
}
},
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/eventsource-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
@@ -1252,6 +1616,94 @@
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1270,6 +1722,28 @@
}
}
},
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/fix-dts-default-cjs-exports": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz",
@@ -1282,6 +1756,26 @@
"rollup": "^4.34.8"
}
},
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1297,14 +1791,201 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/idb": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
- "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
+ "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==",
"dev": true,
- "license": "ISC"
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
- "node_modules/joycon": {
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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/idb": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
+ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
@@ -1321,6 +2002,20 @@
"dev": true,
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -1361,6 +2056,70 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/mlly": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
@@ -1393,6 +2152,16 @@
"thenify-all": "^1.0.0"
}
},
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "dev": true,
+ "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",
@@ -1403,6 +2172,73 @@
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -1440,6 +2276,16 @@
"node": ">= 6"
}
},
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
@@ -1511,6 +2357,67 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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/react": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
@@ -1548,6 +2455,16 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -1603,6 +2520,30 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -1610,6 +2551,159 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@@ -1620,6 +2714,16 @@
"node": ">= 12"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@@ -1690,6 +2794,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -1760,6 +2874,39 @@
}
}
},
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1781,6 +2928,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -1788,6 +2945,49 @@
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
@@ -1797,6 +2997,16 @@
"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==",
+ "dev": true,
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25.28 || ^4"
+ }
}
}
}
diff --git a/package.json b/package.json
index 5c42856..85a3f9d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@dudko.dev/agent-web-react",
- "version": "0.0.3",
+ "version": "0.0.4",
"description": "React bindings for @dudko.dev/agent-web: a headless useAgent hook, an AgentProvider context, and optional pre-styled components (chat panel, plan/step view, BYOK key form, WebLLM load bar) that connect the in-browser LLM agent to any React site. UI you can drop in — or a headless reducer you can build your own around.",
"type": "module",
"sideEffects": [
@@ -104,12 +104,13 @@
"node": ">=18"
},
"peerDependencies": {
- "@dudko.dev/agent-web": ">=0.0.6",
+ "@dudko.dev/agent-web": ">=0.0.12",
"react": ">=18",
"react-dom": ">=18"
},
"devDependencies": {
- "@dudko.dev/agent-web": "^0.0.7",
+ "@dudko.dev/agent-web": "^0.0.12",
+ "@modelcontextprotocol/sdk": "^1.30.0",
"@types/node": "^22.9.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
diff --git a/src/hooks/use-mcp.ts b/src/hooks/use-mcp.ts
new file mode 100644
index 0000000..af79cfa
--- /dev/null
+++ b/src/hooks/use-mcp.ts
@@ -0,0 +1,550 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import type { AgentToolSet } from '@dudko.dev/agent-web'
+import type {
+ BrowserOAuthProvider,
+ ConnectedMcp,
+ McpCatalogEntry,
+ McpModule,
+ McpOAuthCallback,
+ McpServerResult,
+} from '../mcp-types.js'
+
+export type McpStatus = 'idle' | 'connecting' | 'connected' | 'needs-authorization' | 'error'
+
+export interface McpConnectOptions {
+ /** The server's StreamableHTTP endpoint. */
+ url: string
+ /** Prefix for the discovered tool names ("__"). Default 'mcp'. */
+ name?: string
+ /** Static headers, e.g. `{ Authorization: 'Bearer …' }`. Ignored when `oauth` is set. */
+ headers?: Record
+ /**
+ * Authenticate with OAuth 2.1 + dynamic client registration. Requires a core
+ * that ships the OAuth provider; check `oauthSupported` before offering it.
+ */
+ oauth?: boolean | { redirectUrl?: string; scope?: string; clientName?: string }
+}
+
+export interface UseMcpOptions {
+ /** Client name reported to the MCP server during initialize. */
+ clientName?: string
+ /** Called with connector log lines — handy while debugging a server. */
+ onLog?: (level: string, message: string) => void
+}
+
+export interface UseMcpReturn {
+ status: McpStatus
+ /** Merge into `BrowserAgentConfig.tools` once `status === 'connected'`. */
+ tools?: AgentToolSet
+ catalog: McpCatalogEntry[]
+ error?: string
+ /** Where to send the user when `status === 'needs-authorization'`. */
+ authorizationUrl?: string
+ /**
+ * Whether the installed core exposes the OAuth API. `undefined` until the
+ * optional subpath has been loaded once — call `checkOAuthSupport()` to
+ * resolve it before offering OAuth in your UI.
+ */
+ oauthSupported?: boolean
+ /** True while an OAuth redirect is being finished on mount. */
+ completingAuthorization: boolean
+ connect: (options: McpConnectOptions) => Promise
+ disconnect: () => Promise
+ /** Navigate to the authorization server. Call it from a user gesture. */
+ authorize: () => void
+ /** Drop the stored tokens and dynamic registration for the current server. */
+ forgetAuthorization: () => Promise
+ /** Load the optional subpath and report whether it can do OAuth. */
+ checkOAuthSupport: () => Promise
+}
+
+const OAUTH_UNSUPPORTED =
+ 'The installed @dudko.dev/agent-web has no MCP OAuth support. Upgrade the core to a version exporting BrowserOAuthProvider, or use a static Authorization header.'
+
+const PENDING_KEY = 'agent-web-react:mcp:pending'
+const OAUTH_KEY = 'agent-web-react:mcp:oauth'
+
+interface OAuthRecord {
+ url: string
+ name?: string
+ redirectUrl: string
+ scope?: string
+ clientName?: string
+ /** When the redirect was started, so a stale record can't claim a callback. */
+ startedAt?: number
+}
+
+// A round-trip to an authorization server and back takes seconds. Anything
+// older than this is not the flow that produced the ?code= we are looking at —
+// most likely another sign-in on the same page — so we leave it alone.
+const PENDING_TTL_MS = 10 * 60 * 1000
+
+let modulePromise: Promise | undefined
+
+/**
+ * Load the core's optional `./mcp` subpath. Kept dynamic so apps that never
+ * touch MCP don't pull `@modelcontextprotocol/sdk` into their bundle, and so a
+ * core without the OAuth half degrades to a clear message instead of a crash.
+ */
+const loadMcp = (): Promise => {
+ modulePromise ??= import('@dudko.dev/agent-web/mcp')
+ .then((m) => m as unknown as McpModule)
+ .catch((err: unknown) => {
+ // A rejected promise is neither null nor undefined, so ??= would cache
+ // the failure forever — one flaky chunk fetch would kill MCP for the tab.
+ modulePromise = undefined
+ throw err
+ })
+ return modulePromise
+}
+
+/** Map a single server's connect outcome onto the hook's status. Pure. */
+export const describeMcpResult = (
+ result: McpServerResult | undefined,
+): { status: McpStatus; error?: string } => {
+ if (!result) return { status: 'error', error: 'The connector returned no result for the server' }
+ if (result.connected) return { status: 'connected' }
+ if (result.needsAuthorization) return { status: 'needs-authorization' }
+ return { status: 'error', error: result.error ?? 'Could not connect to the MCP server' }
+}
+
+const OAUTH_PARAMS = ['code', 'state', 'error', 'error_description', 'iss']
+
+/**
+ * Read the authorization-code parameters out of a URL **synchronously**.
+ *
+ * The core exports an equivalent, but it lives behind a dynamic import: by the
+ * time that import resolves, React has flushed the rest of the app's effects,
+ * and any one of them may have rewritten `location` (the demo's own view
+ * router did exactly that). The callback has to be captured before the first
+ * await, so this parser is duplicated here on purpose.
+ *
+ * Reads the query string and, for hash-routed apps, the fragment. Returns
+ * undefined when neither a `code` nor an `error` is present.
+ */
+export const readCallbackParams = (href?: string): McpOAuthCallback | undefined => {
+ const target =
+ href ?? (typeof globalThis.location !== 'undefined' ? globalThis.location.href : undefined)
+ if (!target) return undefined
+ let url: URL
+ try {
+ url = new URL(target)
+ } catch {
+ return undefined
+ }
+ const params = new URLSearchParams(url.search)
+ const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash
+ const q = hash.indexOf('?')
+ if (q >= 0) {
+ for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {
+ if (!params.has(k)) params.set(k, v)
+ }
+ }
+ const code = params.get('code') ?? undefined
+ const error = params.get('error') ?? undefined
+ if (!code && !error) return undefined
+ return {
+ code,
+ state: params.get('state') ?? undefined,
+ error,
+ errorDescription: params.get('error_description') ?? undefined,
+ }
+}
+
+/**
+ * Remove the OAuth response parameters from a URL, leaving the rest intact —
+ * query string and fragment alike. The code is single-use and lands in history,
+ * referrers and screenshots, so it goes as soon as it has been read.
+ */
+export const stripOAuthParams = (href: string): string => {
+ const url = new URL(href)
+ for (const key of OAUTH_PARAMS) url.searchParams.delete(key)
+ const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash
+ const q = hash.indexOf('?')
+ if (q >= 0) {
+ const params = new URLSearchParams(hash.slice(q + 1))
+ for (const key of OAUTH_PARAMS) params.delete(key)
+ const rest = params.toString()
+ url.hash = rest ? `${hash.slice(0, q)}?${rest}` : hash.slice(0, q)
+ }
+ return url.toString()
+}
+
+// An authorization code is single-use, and React StrictMode mounts every effect
+// twice in development. Claiming happens synchronously, before any await, so
+// the second pass cannot race the first into the token endpoint (where it would
+// lose the state check and report a CSRF failure on a perfectly good flow).
+const claimedCallbacks = new Set()
+
+export const claimOAuthCallback = (callback: McpOAuthCallback): boolean => {
+ const key = `${callback.code ?? ''}|${callback.state ?? ''}|${callback.error ?? ''}`
+ if (claimedCallbacks.has(key)) return false
+ claimedCallbacks.add(key)
+ return true
+}
+
+const readRecord = (key: string): T | undefined => {
+ try {
+ const raw = globalThis.localStorage?.getItem(key)
+ return raw ? (JSON.parse(raw) as T) : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const writeRecord = (key: string, value: unknown): void => {
+ try {
+ if (value) globalThis.localStorage?.setItem(key, JSON.stringify(value))
+ else globalThis.localStorage?.removeItem(key)
+ } catch {
+ // Private mode / storage disabled: the OAuth round-trip simply won't
+ // resume automatically, which the UI already handles.
+ }
+}
+
+// A redirect_uri must not carry a fragment (RFC 6749 §3.1.2), and it has to
+// match the registered value byte-for-byte, so query and hash are dropped.
+const defaultRedirectUrl = (): string =>
+ typeof globalThis.location === 'undefined'
+ ? ''
+ : `${globalThis.location.origin}${globalThis.location.pathname}`
+
+/**
+ * Did this error come from the server refusing our authorization?
+ *
+ * Identity, not strings: the SDK's `UnauthorizedError` never assigns
+ * `this.name` (so it reads as "Error"), and its message would also match a
+ * tool whose own error text mentions "unauthorized" — the connector throws a
+ * failing tool's text verbatim. The class is re-exported by the core's `./mcp`
+ * subpath for exactly this; `constructor.name` is the fallback for a core too
+ * old to export it.
+ */
+export const isUnauthorizedError = (err: unknown, mod?: McpModule): boolean => {
+ if (!(err instanceof Error)) return false
+ if (mod?.UnauthorizedError && err instanceof mod.UnauthorizedError) return true
+ return err.constructor?.name === 'UnauthorizedError'
+}
+
+/**
+ * Wrap each tool so a mid-session authorization failure is visible. Without
+ * this the panel keeps reporting "connected" while every call fails: the core
+ * refreshes silently on a 401, but once the refresh token is gone (revoked,
+ * expired) it can only ask for a new authorization.
+ */
+const watchAuthorization = (
+ tools: AgentToolSet,
+ mod: McpModule,
+ onUnauthorized: () => void,
+): AgentToolSet => {
+ const entries = Object.entries(tools as Record).map(([name, value]) => {
+ const tool = value as { execute?: (args: unknown, options: unknown) => unknown }
+ if (typeof tool.execute !== 'function') return [name, value]
+ const execute = tool.execute.bind(tool)
+ const watched = (args: unknown, options: unknown): unknown => {
+ // Deliberately not an async function: the AI SDK inspects what `execute`
+ // returns SYNCHRONOUSLY to decide whether a tool streams (isAsyncIterable),
+ // so wrapping everything in a promise would collapse a streaming tool into
+ // a single opaque result.
+ try {
+ const out = execute(args, options)
+ if (out && typeof (out as Promise).then === 'function') {
+ return (out as Promise).then(undefined, (err: unknown) => {
+ if (isUnauthorizedError(err, mod)) onUnauthorized()
+ throw err
+ })
+ }
+ return out
+ } catch (err) {
+ if (isUnauthorizedError(err, mod)) onUnauthorized()
+ throw err
+ }
+ }
+ return [name, { ...(value as object), execute: watched }]
+ })
+ return Object.fromEntries(entries) as AgentToolSet
+}
+
+/**
+ * Connect the browser agent to a remote MCP server the user names at runtime —
+ * static header, or full OAuth 2.1 with dynamic client registration.
+ *
+ * The OAuth round-trip leaves the page, so the hook persists what it needs to
+ * resume: on mount it detects `?code=…`, finishes the exchange, cleans the URL
+ * and reconnects, all before the app renders anything MCP-related.
+ *
+ * ```tsx
+ * const mcp = useMcp()
+ * // await mcp.connect({ url, oauth: true })
+ * // mcp.status === 'needs-authorization' &&
+ * //
+ * ```
+ */
+export const useMcp = (options: UseMcpOptions = {}): UseMcpReturn => {
+ const [status, setStatus] = useState('idle')
+ const [tools, setTools] = useState(undefined)
+ const [catalog, setCatalog] = useState([])
+ const [error, setError] = useState(undefined)
+ const [authorizationUrl, setAuthorizationUrl] = useState(undefined)
+ const [oauthSupported, setOauthSupported] = useState(undefined)
+ const [completingAuthorization, setCompletingAuthorization] = useState(
+ () => readCallbackParams() !== undefined,
+ )
+
+ const connectionRef = useRef(undefined)
+ const providerRef = useRef(undefined)
+ // Bumped by every connect / disconnect / unmount. A handshake that finishes
+ // after its epoch has passed closes itself instead of writing state or
+ // leaking an open MCP session onto a dead component.
+ const epochRef = useRef(0)
+ // Options land in a ref so `connect` keeps a stable identity: it is a natural
+ // dependency of effects in host components.
+ const optionsRef = useRef(options)
+ optionsRef.current = options
+
+ const closeConnection = useCallback(async () => {
+ const open = connectionRef.current
+ connectionRef.current = undefined
+ if (open) await open.close().catch(() => {})
+ }, [])
+
+ const checkOAuthSupport = useCallback(async () => {
+ try {
+ const mod = await loadMcp()
+ const supported = typeof mod.BrowserOAuthProvider === 'function'
+ setOauthSupported(supported)
+ return supported
+ } catch {
+ setOauthSupported(false)
+ return false
+ }
+ }, [])
+
+ const connect = useCallback(
+ async (opts: McpConnectOptions) => {
+ const epoch = ++epochRef.current
+ const current = () => epochRef.current === epoch
+ await closeConnection()
+ if (!current()) return
+ setStatus('connecting')
+ setError(undefined)
+ setAuthorizationUrl(undefined)
+ setTools(undefined)
+ setCatalog([])
+
+ try {
+ const mod = await loadMcp()
+ setOauthSupported(typeof mod.BrowserOAuthProvider === 'function')
+ const name = opts.name ?? 'mcp'
+ let authProvider: BrowserOAuthProvider | undefined
+
+ if (opts.oauth) {
+ if (!mod.BrowserOAuthProvider) throw new Error(OAUTH_UNSUPPORTED)
+ const oauth = typeof opts.oauth === 'object' ? opts.oauth : {}
+ const record: OAuthRecord = {
+ url: opts.url,
+ name,
+ redirectUrl: oauth.redirectUrl ?? defaultRedirectUrl(),
+ scope: oauth.scope,
+ clientName: oauth.clientName ?? optionsRef.current.clientName,
+ startedAt: Date.now(),
+ }
+ authProvider = new mod.BrowserOAuthProvider({
+ serverUrl: record.url,
+ redirectUrl: record.redirectUrl,
+ clientName: record.clientName,
+ scope: record.scope,
+ })
+ providerRef.current = authProvider
+ // `pending` resumes the redirect; `oauth` outlives it so tokens stay
+ // addressable for forgetAuthorization() after a reload.
+ writeRecord(PENDING_KEY, record)
+ writeRecord(OAUTH_KEY, record)
+ } else {
+ providerRef.current = undefined
+ }
+
+ const connection = await mod.connectMcpHttp(
+ {
+ [name]: {
+ url: opts.url,
+ headers: opts.oauth ? undefined : opts.headers,
+ authProvider,
+ },
+ },
+ { clientName: optionsRef.current.clientName, onLog: optionsRef.current.onLog },
+ )
+
+ if (!current()) {
+ // Unmounted, or another connect started while we were shaking hands.
+ await connection.close().catch(() => {})
+ return
+ }
+
+ const outcome = describeMcpResult(connection.results[0])
+ if (outcome.status !== 'connected') {
+ await connection.close().catch(() => {})
+ if (outcome.status === 'needs-authorization') {
+ const url = authProvider?.authorizationUrl
+ setAuthorizationUrl(url ? String(url) : undefined)
+ setStatus('needs-authorization')
+ return
+ }
+ throw new Error(outcome.error)
+ }
+
+ connectionRef.current = connection
+ // The round-trip is over: a stale record would later make an unrelated
+ // ?code= on this page look like ours.
+ writeRecord(PENDING_KEY, undefined)
+ setTools(
+ watchAuthorization(connection.tools, mod, () => {
+ if (!current()) return
+ const url = providerRef.current?.authorizationUrl
+ if (url) setAuthorizationUrl(String(url))
+ setStatus('needs-authorization')
+ }),
+ )
+ setCatalog(connection.catalog)
+ setStatus('connected')
+ } catch (err) {
+ if (!current()) return
+ setError(err instanceof Error ? err.message : String(err))
+ setStatus('error')
+ }
+ },
+ [closeConnection],
+ )
+
+ const disconnect = useCallback(async () => {
+ epochRef.current++
+ await closeConnection()
+ setTools(undefined)
+ setCatalog([])
+ setAuthorizationUrl(undefined)
+ setError(undefined)
+ setStatus('idle')
+ }, [closeConnection])
+
+ const authorize = useCallback(() => {
+ if (authorizationUrl && typeof globalThis.location !== 'undefined') {
+ globalThis.location.href = authorizationUrl
+ }
+ }, [authorizationUrl])
+
+ const forgetAuthorization = useCallback(async () => {
+ const record = readRecord(OAUTH_KEY)
+ writeRecord(PENDING_KEY, undefined)
+ writeRecord(OAUTH_KEY, undefined)
+ // After a reload there is no live provider, but the tokens are still in the
+ // vault — rebuild one for the same server so "forget" actually forgets
+ // rather than only resetting the UI.
+ let provider = providerRef.current
+ if (!provider && record) {
+ const mod = await loadMcp().catch(() => undefined)
+ if (mod?.BrowserOAuthProvider) {
+ provider = new mod.BrowserOAuthProvider({
+ serverUrl: record.url,
+ redirectUrl: record.redirectUrl,
+ clientName: record.clientName,
+ scope: record.scope,
+ })
+ }
+ }
+ await provider?.reset().catch(() => {})
+ providerRef.current = undefined
+ await disconnect()
+ }, [disconnect])
+
+ // Resume an OAuth round-trip: the authorization server has just sent the user
+ // back with ?code=… and this component is mounting for the first time.
+ useEffect(() => {
+ // Everything up to the first await runs synchronously, on purpose: React
+ // flushes the remaining passive effects (which may rewrite location) before
+ // an awaited continuation resumes, and StrictMode runs this effect twice.
+ const callback = readCallbackParams()
+ if (!callback) {
+ setCompletingAuthorization(false)
+ return
+ }
+ const pending = readRecord(PENDING_KEY)
+ // A record only proves that WE started a flow, not that THIS callback is
+ // its answer — so an expired one is treated as somebody else's. Either way
+ // the URL is left exactly as we found it: another sign-in flow on this page
+ // may still need those parameters.
+ const fresh =
+ pending !== undefined &&
+ (pending.startedAt === undefined || Date.now() - pending.startedAt < PENDING_TTL_MS)
+ if (!pending || !fresh) {
+ if (pending) writeRecord(PENDING_KEY, undefined)
+ setCompletingAuthorization(false)
+ return
+ }
+ if (!claimOAuthCallback(callback)) {
+ setCompletingAuthorization(false)
+ return
+ }
+ // Ours, and claimed: the code is in hand, so clear it from the address bar
+ // now rather than after the reconnect.
+ if (typeof globalThis.history !== 'undefined') {
+ globalThis.history.replaceState(null, '', stripOAuthParams(globalThis.location.href))
+ }
+
+ void (async () => {
+ try {
+ const mod = await loadMcp()
+ setOauthSupported(typeof mod.BrowserOAuthProvider === 'function')
+ if (!mod.BrowserOAuthProvider || !mod.finishMcpOAuth) throw new Error(OAUTH_UNSUPPORTED)
+ const provider = new mod.BrowserOAuthProvider({
+ serverUrl: pending.url,
+ redirectUrl: pending.redirectUrl ?? defaultRedirectUrl(),
+ clientName: pending.clientName,
+ scope: pending.scope,
+ })
+ providerRef.current = provider
+ await mod.finishMcpOAuth(provider, callback)
+ writeRecord(PENDING_KEY, undefined)
+ // NOT gated on `cancelled`. StrictMode's simulated cleanup sets it in
+ // development, and the second setup cannot take over — the code has
+ // already been claimed and stripped from the URL — so bailing here
+ // would finish the authorization and then never connect. `connect` is
+ // epoch-guarded and a setState after a real unmount is a no-op, so
+ // letting it run is safe either way.
+ //
+ // Hand back to `connect`, which owns the status from here — a slow or
+ // hung handshake must not leave the UI stuck on "finishing".
+ setCompletingAuthorization(false)
+ await connect({ url: pending.url, name: pending.name, oauth: true })
+ } catch (err) {
+ writeRecord(PENDING_KEY, undefined)
+ setError(err instanceof Error ? err.message : String(err))
+ setStatus('error')
+ } finally {
+ setCompletingAuthorization(false)
+ }
+ })()
+ // Runs once: `connect` is stable and the callback exists only on first load.
+ }, [connect])
+
+ useEffect(
+ () => () => {
+ epochRef.current++
+ void closeConnection()
+ },
+ [closeConnection],
+ )
+
+ return {
+ status,
+ tools,
+ catalog,
+ error,
+ authorizationUrl,
+ oauthSupported,
+ completingAuthorization,
+ connect,
+ disconnect,
+ authorize,
+ forgetAuthorization,
+ checkOAuthSupport,
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 5e7f771..5b0339e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,6 +3,22 @@ export { useAgent } from './hooks/use-agent.js'
export type { UseAgentOptions, UseAgentReturn } from './hooks/use-agent.js'
export { useCredentials } from './hooks/use-credentials.js'
export type { UseCredentialsReturn } from './hooks/use-credentials.js'
+export {
+ useMcp,
+ claimOAuthCallback,
+ describeMcpResult,
+ isUnauthorizedError,
+ readCallbackParams,
+ stripOAuthParams,
+} from './hooks/use-mcp.js'
+export type { McpConnectOptions, McpStatus, UseMcpOptions, UseMcpReturn } from './hooks/use-mcp.js'
+export type {
+ ConnectedMcp,
+ McpCatalogEntry,
+ McpOAuthCallback,
+ McpServerResult,
+ McpModule,
+} from './mcp-types.js'
export { useWebLLMModel } from './hooks/use-webllm-model.js'
export type {
UseWebLLMModelReturn,
diff --git a/src/mcp-types.ts b/src/mcp-types.ts
new file mode 100644
index 0000000..64c5bf8
--- /dev/null
+++ b/src/mcp-types.ts
@@ -0,0 +1,76 @@
+import type { AgentToolSet } from '@dudko.dev/agent-web'
+
+/**
+ * Structural types for the core's OPTIONAL `@dudko.dev/agent-web/mcp` subpath.
+ *
+ * The subpath is loaded with a dynamic import and typed here rather than
+ * imported, for two reasons: it drags in `@modelcontextprotocol/sdk` (an
+ * optional peer of the core, which apps without MCP must not have to install),
+ * and the OAuth half only exists in newer cores while this package supports
+ * `@dudko.dev/agent-web >= 0.0.6`. `useMcp` feature-detects at runtime and
+ * reports `oauthSupported: false` on an older core instead of crashing.
+ */
+
+export interface McpCatalogEntry {
+ name: string
+ description: string
+ server: string
+}
+
+export interface McpServerResult {
+ name: string
+ connected: boolean
+ error?: string
+ needsAuthorization?: boolean
+}
+
+export interface ConnectedMcp {
+ tools: AgentToolSet
+ catalog: McpCatalogEntry[]
+ results: McpServerResult[]
+ refreshServer?: (name: string) => Promise
+ close: () => Promise
+}
+
+export interface McpHttpServerConfig {
+ url: string
+ headers?: Record
+ authProvider?: unknown
+}
+
+export interface McpOAuthCallback {
+ code?: string
+ state?: string
+ error?: string
+ errorDescription?: string
+}
+
+export interface BrowserOAuthProvider {
+ serverUrl: string
+ authorizationUrl?: URL
+ isAuthorized(): Promise
+ reset(): Promise
+}
+
+export interface BrowserOAuthProviderOptions {
+ serverUrl: string
+ redirectUrl: string
+ clientName?: string
+ scope?: string
+}
+
+/** The shape of the subpath module, as far as this package uses it. */
+export interface McpModule {
+ connectMcpHttp: (
+ servers: Record,
+ opts?: { clientName?: string; onLog?: (level: string, message: string) => void },
+ ) => Promise
+ BrowserOAuthProvider?: new (opts: BrowserOAuthProviderOptions) => BrowserOAuthProvider
+ readOAuthCallback?: (input?: string | URL) => McpOAuthCallback | undefined
+ finishMcpOAuth?: (provider: BrowserOAuthProvider, callback: McpOAuthCallback) => Promise
+ /**
+ * The SDK's error class, re-exported by the core so an authorization failure
+ * can be recognised by identity. Absent on cores older than 0.0.12.
+ */
+ UnauthorizedError?: new (message?: string) => Error
+}
diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts
new file mode 100644
index 0000000..76a8730
--- /dev/null
+++ b/tests/mcp.test.ts
@@ -0,0 +1,140 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import {
+ claimOAuthCallback,
+ describeMcpResult,
+ isUnauthorizedError,
+ readCallbackParams,
+ stripOAuthParams,
+} from '../dist/index.js'
+
+// The connect/OAuth machinery lives in the core and is covered there; what this
+// package owns is the mapping from a connect outcome to UI state, and the URL
+// hygiene around the redirect. Both are pure, so they are tested without React.
+
+test('describeMcpResult: a connected server yields the connected status', () => {
+ assert.deepEqual(describeMcpResult({ name: 'mcp', connected: true }), { status: 'connected' })
+})
+
+test('describeMcpResult: needsAuthorization outranks the accompanying error text', () => {
+ assert.deepEqual(
+ describeMcpResult({
+ name: 'mcp',
+ connected: false,
+ needsAuthorization: true,
+ error: 'Unauthorized',
+ }),
+ { status: 'needs-authorization' },
+ )
+})
+
+test('describeMcpResult: a plain failure carries the server error through', () => {
+ assert.deepEqual(describeMcpResult({ name: 'mcp', connected: false, error: 'DNS exploded' }), {
+ status: 'error',
+ error: 'DNS exploded',
+ })
+})
+
+test('describeMcpResult: a failure with no message still gets a readable one', () => {
+ const { status, error } = describeMcpResult({ name: 'mcp', connected: false })
+ assert.equal(status, 'error')
+ assert.match(error ?? '', /Could not connect/)
+})
+
+test('describeMcpResult: a missing result is an error, not a crash', () => {
+ const { status, error } = describeMcpResult(undefined)
+ assert.equal(status, 'error')
+ assert.match(error ?? '', /no result/)
+})
+
+test('stripOAuthParams: drops the single-use response params, keeps everything else', () => {
+ assert.equal(
+ stripOAuthParams('https://app.test/demo/?code=abc&state=xyz&iss=as&view=mcp#/panel'),
+ 'https://app.test/demo/?view=mcp#/panel',
+ )
+})
+
+test('stripOAuthParams: drops an error response too', () => {
+ assert.equal(
+ stripOAuthParams('https://app.test/?error=access_denied&error_description=nope'),
+ 'https://app.test/',
+ )
+})
+
+test('stripOAuthParams: leaves a clean URL untouched', () => {
+ assert.equal(stripOAuthParams('https://app.test/demo/'), 'https://app.test/demo/')
+})
+
+// The callback has to be read synchronously, before the hook's first await:
+// React flushes the rest of the app's effects in between, and one of them may
+// rewrite location (the demo's own view router did exactly that).
+
+test('readCallbackParams: reads a query-string callback', () => {
+ assert.deepEqual(readCallbackParams('https://app.test/demo/?code=abc&state=xyz'), {
+ code: 'abc',
+ state: 'xyz',
+ error: undefined,
+ errorDescription: undefined,
+ })
+})
+
+test('readCallbackParams: reads a callback carried in a hash route', () => {
+ assert.deepEqual(readCallbackParams('https://app.test/#/cb?code=abc&state=xyz'), {
+ code: 'abc',
+ state: 'xyz',
+ error: undefined,
+ errorDescription: undefined,
+ })
+})
+
+test('readCallbackParams: reads a denial, and ignores a URL carrying neither', () => {
+ const denied = readCallbackParams('https://app.test/?error=access_denied&error_description=nope')
+ assert.equal(denied?.error, 'access_denied')
+ assert.equal(denied?.errorDescription, 'nope')
+ assert.equal(readCallbackParams('https://app.test/demo/?view=mcp'), undefined)
+ assert.equal(readCallbackParams('not a url'), undefined)
+})
+
+test('stripOAuthParams: clears a callback carried in the fragment too', () => {
+ assert.equal(
+ stripOAuthParams('https://app.test/#/cb?code=abc&state=xyz&tab=tools'),
+ 'https://app.test/#/cb?tab=tools',
+ )
+ assert.equal(stripOAuthParams('https://app.test/#/cb?code=abc'), 'https://app.test/#/cb')
+})
+
+test('claimOAuthCallback: a code can only be claimed once', () => {
+ const callback = { code: 'single-use', state: 'st' }
+ // StrictMode mounts every effect twice in development; the second pass must
+ // not race the first into the token endpoint, where it would lose the
+ // single-use state check and report a CSRF failure on a good flow.
+ assert.equal(claimOAuthCallback(callback), true)
+ assert.equal(claimOAuthCallback({ ...callback }), false)
+ assert.equal(claimOAuthCallback({ code: 'another', state: 'st' }), true)
+})
+
+// The SDK's UnauthorizedError never assigns `this.name`, so it reads as
+// "Error" — a name check silently never fires, which is how the mid-session
+// re-authorization prompt shipped as dead code.
+
+test('isUnauthorizedError: matches the core-exported class by identity', () => {
+ class UnauthorizedError extends Error {}
+ const mod = { UnauthorizedError } as never
+ assert.equal(isUnauthorizedError(new UnauthorizedError('Unauthorized'), mod), true)
+ assert.equal(isUnauthorizedError(new Error('Unauthorized'), mod), false)
+})
+
+test('isUnauthorizedError: falls back to the constructor name on an older core', () => {
+ class UnauthorizedError extends Error {}
+ const err = new UnauthorizedError()
+ assert.equal(err.name, 'Error', 'guards the assumption behind the fallback')
+ assert.equal(isUnauthorizedError(err), true)
+})
+
+test('isUnauthorizedError: a tool whose own error mentions "unauthorized" is not auth', () => {
+ // The connector throws a failing tool's text verbatim, so message matching
+ // would flip the UI into an auth prompt for an ordinary tool failure.
+ assert.equal(isUnauthorizedError(new Error('unauthorized: quota exceeded')), false)
+ assert.equal(isUnauthorizedError('not an error'), false)
+ assert.equal(isUnauthorizedError(undefined), false)
+})