commit d5c4d2006cf5f6f4cdbc99d331fd6a84be5a8960 Author: oliver Date: Sat Sep 12 08:51:32 2026 +0800 Publish dsh-search-mcp fork for newer DSH and Bailian WebSearch MCP. Based on gxpppp/dsh-search-mcp; includes DSH web settings fixes, Clash fake-IP URL policy, and Bailian default server patch. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fb7a06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +*.log +*.tgz +.DS_Store + +# Local security scan state and private project handoff. +.mimosa/ +PROJECT_CONTEXT.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0f5049 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# dsh-search-mcp + +用搜索类 MCP 服务器完整替代 DeepSeek Harness(DSH)内置网页搜索的独立插件。 + +> 当前兼容基线:DeepSeek Harness `0.1.1-rc.2`,Node.js 20 或更高版本。 + +## 功能 + +- 模型侧继续使用原生 `web_search`,插件只替换底层 search provider。 +- 支持 Tavily、Brave、Exa、Perplexity、DuckDuckGo 和自定义 HTTP/stdio MCP。 +- 已知 provider 只需选择服务商并填写 CDKey/API key,不需要填写 URL、命令、鉴权参数或工具名。 +- 自定义 MCP 保留 URL、stdio 命令、鉴权方式和工具名等高级配置。 +- 密钥通过 DSH credentials domain 写入;设置读取接口只返回是否已配置,不返回密钥值。 +- DSH RC2 支持一次 `web_search` 提交多个查询,默认上限为 4。 +- 卸载插件后 bundle 覆盖层随之移除,DSH 内置搜索组合恢复。 + +## 安装 + +```powershell +git clone https://github.com/gxpppp/dsh-search-mcp.git +cd dsh-search-mcp +npm install +dsh plugin --profile web add link: +dsh web +``` + +`link:` 会让源码更新直接作用于 profile。修改或升级浏览器 bundle 后需要重启 DSH Web 并刷新页面。 + +如果 profile 中已有独立搜索 MCP 行,建议先移除重复入口,避免同时暴露 `mcp__...` 工具和本插件提供的 `web_search`。 + +## 配置 + +打开: + +**设置 → 插件 → 插件配置 → 搜索 MCP** + +### 已知 provider + +1. 点击 Tavily、Brave、Exa、Perplexity 或 DuckDuckGo 快捷按钮。 +2. 展开服务器行。 +3. 对需要凭据的 provider 填写 CDKey/API key,然后保存。 +4. DuckDuckGo 无需 key。 + +已知 provider 的 endpoint、transport、鉴权方式、工具名和结果参数由 Host catalog 固定管理,设置页不会自动填入或显示链接。保存 CDKey 后,客户端先调用 `credentials.set`,再将生成的 credential reference 写入服务器设置;密钥本身不会写回普通 settings 字段。 + +也可以预先在 `$DSH_HOME/.credentials.yaml` 中保存凭据,再在设置页填写引用名: + +```yaml +TAVILY_API_KEY: +EXA_API_KEY: +PERPLEXITY_API_KEY: +BRAVE_API_KEY: +``` + +RC2 的 `credentials/reference-updated` 事件会刷新设置卡片中的“已配置/未配置”状态,但不会传输密钥值。卡片按 RC2 每批最多 64 个引用的限制分批读取状态。保存多个字段失败时会逆序恢复已写入的 settings,并清理本次新建的 credential reference;由于 RC2 不允许读回已有密钥,覆盖一个此前已配置的引用后无法跨 credentials/settings 做值级回滚。 + +### 自定义 MCP + +添加 `custom` 服务器后,可配置: + +| 字段 | 说明 | +|---|---| +| `id` | 服务器唯一标识,供 `defaultServer` 引用 | +| `transport` | `http`(Streamable HTTP)或 `stdio` | +| `url` | 仅 HTTP 自定义 MCP 使用 | +| `command` / `args` | 仅 stdio 自定义 MCP 使用 | +| `apiKey` | 写入方向的 CDKey/API key 输入 | +| `apiKeyEnv` | 环境变量或 DSH credential reference | +| `authStyle` | HTTP 的 `query` 或 `header` | +| `authParam` | query/header 参数名;stdio 下作为环境变量名 | +| `toolName` | MCP 搜索工具名称 | +| `maxResults` | 单服务器结果数覆盖 | + +### 全局选项 + +| 字段 | 说明 | +|---|---| +| `defaultServer` | 默认服务器 id;留空时使用第一行 | +| `maxResults` | 全局结果数上限,默认 8,可选 1–50 | +| `searchTimeoutMs` | 每次 MCP 搜索超时,默认 30000 ms;界面以秒显示 | + +## Provider 预设 + +| kind | Host 管理的连接 | 凭据 | 搜索工具 | 结果数参数 | +|---|---|---|---|---| +| `tavily` | hosted Streamable HTTP | CDKey/API key | `tavily_search` | `max_results` | +| `brave` | `@brave/brave-search-mcp-server@2.1.3` stdio | `BRAVE_API_KEY` | `brave_web_search` | `count` | +| `exa` | hosted Streamable HTTP | `x-api-key` | `web_search_exa` | `numResults` | +| `perplexity` | hosted Streamable HTTP | Bearer token | `perplexity_search` | `max_results` | +| `duckduckgo` | `duckduckgo-mcp-server@0.1.2` stdio | 无 | `duckduckgo_web_search` | `count` | +| `custom` | 用户配置 | 用户配置 | 用户配置 | 无预设 | + +旧配置中的 known-provider URL、transport、auth 和 tool 字段仍可被 schema 读取,但运行时会忽略它们;下一次保存服务器列表时会清理这些冗余字段。只有 `custom` 使用用户提供的连接信息。 + +插件在调用 known provider 前会把结果数限制到上游 MCP schema 接受的范围:Tavily 为 5–20,Brave、Perplexity 和 DuckDuckGo 为 1–20;Exa 当前保留插件的 1–50 范围。该限制只影响传给上游的参数,最终返回数量仍会受到插件全局/单服务器限制和实际 agent preset 的 `tool-web.searchMaxResults` 共同约束。 + +## DSH 0.1.1-rc.2 适配 + +- DSH host 依赖精确锁定为 `0.1.1-rc.2`,不使用可能落到旧版本线的子包 `latest`。 +- 设置卡片继续使用 keyed slot:`settings.plugin.item` + `key: "search-mcp"`。 +- 新密钥通过 `credentials.set` 单向写入,凭据状态通过 `credentials.describe` 读取。 +- 监听 RC2 的 `credentials/reference-updated`,外部凭据变更后刷新状态 badge。 +- RC6/RC7 遗留的字面 `apiKey` 仍可由 Host 使用;涉及服务器数组的编辑会阻止不可见旧密钥被意外删除,并要求先迁移。 +- 普通全局字段修改不会重写 `servers` 数组。 +- `tool-web.searchMaxQueries` 配置为 4,与 RC2 默认多查询能力一致。 + +## URL 安全策略 + +所有自定义 HTTP MCP 请求在联网前执行安全校验: + +- 只允许 `http:` 和 `https:`,拒绝 userinfo 与非规范 IPv4 表示。 +- 拒绝 localhost、环回、RFC1918 私网、链路本地、CGNAT、benchmark、文档/测试、多播、保留和广播地址。 +- IPv4-mapped IPv6 先映射为 IPv4 再判断;IPv4-compatible IPv6、IPv6 ULA、link-local、NAT64/转换、Teredo、6to4、文档和保留范围同样拒绝。 +- 域名会解析全部 A/AAAA 结果;任意一个结果不公开可路由时整体拒绝,DNS 等待也受同一个搜索 AbortSignal/超时约束。 +- 每次搜索使用独占 Undici Agent 和预解析地址的 pinned lookup,同时保留原始 Host 与 TLS SNI,防止 DNS rebinding。 +- GET、POST、DELETE 和 SSE 重连都通过同一 fetch wrapper,HTTP 重定向设置为 `error`。 +- 先关闭 MCP client,再关闭本次 Agent,不共享连接池。 +- 错误信息不会输出包含 CDKey 的完整 URL。 + +如果代理或 TUN 把公共域名解析到 `198.18.0.0/15` fake-IP,本插件会按 benchmark/test 网段安全拒绝。应让 DSH 进程获得真实公网 DNS 结果,而不是放宽策略。 + +## 组合覆盖 + +插件通过 `cordis.patch.yml`: + +- 注册 `search-mcp` provider。 +- 设置 `web.searchProvider: search-mcp`。 +- 禁用 `web-search-deepseek`。 +- 保持 `web_fetch` 关闭。 +- 请求 `tool-web.searchMaxResults: 50` 和 `searchMaxQueries: 4`。 + +RC2 的 standard、code、cordis agent preset 各自包含 `tool-web` 行,并且都省略了 `searchMaxResults` 和 `searchMaxQueries`,因此实际采用 `dsh-tool-web` 默认值 8 和 4。agent-scoped 工具会遮蔽根层同名工具,所以根层 patch 中的 50 条请求不会提高这些 shipped preset 的实际上限。验证结果上限时必须检查 session 使用的 preset,不能只依据根层 `--dump-config`。 + +## 验证 + +```powershell +npm test +npm run check +npm pack --dry-run +``` + +自动测试覆盖 RC2 依赖锁定、known/custom catalog 边界、CDKey-only 设置结构、凭据事件、旧 secret 保护、结果归一化,以及 URL/DNS/pinning 安全策略。 + +2026-08-30 的隔离 RC2 Web 冒烟检查确认:插件卡片可加载;默认 Tavily 行不显示链接或高级连接字段;DuckDuckGo 摘要显示“无需密钥”,展开后只有 ID、provider 和结果数;测试草稿已放弃且没有写入 settings。无密钥 DuckDuckGo stdio server 能启动并收到正确的 `duckduckgo_web_search`/`count` 调用,但当次公开搜索被 DuckDuckGo 上游异常流量检测拒绝,因此未取得可用于结果归一化验收的真实来源。 + +组合检查: + +```powershell +dsh --profile web --dump-config | + Select-String -Pattern "searchProvider|search-mcp|web-search-deepseek|searchMaxResults|searchMaxQueries" +``` + +预期至少包括: + +- `web.searchProvider: search-mcp` +- `web-search-deepseek.disabled: true` +- `tool-web.disabled: false` +- `fetch: false` + +实际 agent preset 的结果数和多查询上限应在隔离 profile/session 中单独验证。 + +## 故障排查 + +- `no search MCP servers configured`:在设置页添加 provider。 +- `has no API key`:填写 CDKey/API key,或填写已有 credential reference。 +- “凭证未配置”:引用名存在于 settings,但 credentials provider 当前找不到对应值。 +- `defaultServer "x" is not configured`:默认 id 没有匹配任何服务器行。 +- `URL policy` 拒绝:endpoint 非 HTTP(S),或 DNS 结果包含本地、私有、保留/测试地址。 +- stdio 启动失败:确认 Node/npm 可用,且运行环境允许 `npx` 获取或执行对应 MCP 包。 +- 设置页没有 Search MCP 卡片:确认 client bundle 已安装,重启 DSH Web 后强制刷新页面。 +- 返回结果仍被截断:检查实际 agent preset 中的 `tool-web.searchMaxResults`,以及全局/单服务器 `maxResults`。 + +## 卸载 + +```powershell +dsh plugin --profile web remove dsh-search-mcp +``` + +随后重启 DSH Web。不要只禁用 `search-mcp` 行,因为 bundle 还覆盖了 `web`、`web-search-deepseek` 和 `tool-web`;完整卸载 bundle 才会恢复内置组合。 + +## License + +MIT diff --git a/cordis.patch.yml b/cordis.patch.yml new file mode 100644 index 0000000..5b4cb45 --- /dev/null +++ b/cordis.patch.yml @@ -0,0 +1,58 @@ +# dsh-search-mcp bundle layer: applied after dsh-base and dsh-web-app, +# before the profile's own cordis.patch.yml (last write wins per row). +# +# Installing this package therefore REPLACES dsh's built-in web search: +# - the `web` row's searchProvider switches from `deepseek-official` to +# `search-mcp` (the provider registered by this plugin), and +# - the built-in DeepSeek search provider row is disabled. +# The model-facing `web_search` tool keeps its name and presentation; its +# execution now goes through the search MCP server(s) configured below or in +# the web Settings → Plugins → search-mcp section. +# +# Removing this package (dsh plugin --profile web remove dsh-search-mcp) +# drops this whole layer and restores the built-in search exactly. +# +# SECURITY: no API keys are committed to this repository. Server keys are +# supplied at runtime through `apiKeyEnv` — stored in +# `$DSH_HOME/.credentials.yaml` (e.g. `TAVILY_API_KEY: `) — or through +# the web Settings → Plugins → search-mcp section (`apiKey` field). + +- insert: + - id: search-mcp + name: 'dsh-search-mcp' + config: + defaultServer: bailian + maxResults: 8 + searchTimeoutMs: 30000 + servers: + - id: bailian + kind: custom + transport: http + url: https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp + authStyle: header + authParam: Authorization + authPrefix: 'Bearer ' + apiKeyEnv: DASHSCOPE_API_KEY + toolName: bailian_web_search + +- id: web + config: + searchProvider: search-mcp + +- id: web-search-deepseek + disabled: true + +# The model-facing web_search tool is owned by dsh-tool-web (its `Config` +# default is a hard 8-source cap, enforced by dsh-web's seam on EVERY +# request). This plugin takes over result sizing, so raise the tool layer's +# cap to the plugin schema maximum: `search-mcp`'s own maxResults (Settings → +# Plugins → search-mcp) then decides how many sources actually come back. +# `fetch` stays disabled and the base timeout is restated, because a patch +# replaces the targeted row's whole config. +- id: tool-web + disabled: false + config: + fetch: false + searchTimeoutMs: 60000 + searchMaxResults: 50 + searchMaxQueries: 4 diff --git a/lib/catalog.js b/lib/catalog.js new file mode 100644 index 0000000..4cee9e7 --- /dev/null +++ b/lib/catalog.js @@ -0,0 +1,137 @@ +/** + * Search-MCP provider catalog. + * + * Known providers are intentionally connection-opaque to Settings clients: the + * host owns their endpoint, transport, authentication contract and tool name. + * `custom` is the only kind whose connection details come from the user. + */ +export const SEARCH_MCP_CATALOG = { + tavily: { + transport: 'http', + url: 'https://mcp.tavily.com/mcp/', + authStyle: 'query', + authParam: 'tavilyApiKey', + toolName: 'tavily_search', + countArg: 'max_results', + minResults: 5, + maxResultsLimit: 20, + apiKeyEnv: 'TAVILY_API_KEY', + needsKey: true, + }, + brave: { + transport: 'stdio', + command: 'npx', + args: ['-y', '@brave/brave-search-mcp-server@2.1.3'], + authStyle: 'env', + authParam: 'BRAVE_API_KEY', + toolName: 'brave_web_search', + countArg: 'count', + minResults: 1, + maxResultsLimit: 20, + apiKeyEnv: 'BRAVE_API_KEY', + needsKey: true, + }, + exa: { + transport: 'http', + url: 'https://mcp.exa.ai/mcp', + authStyle: 'header', + authParam: 'x-api-key', + toolName: 'web_search_exa', + countArg: 'numResults', + apiKeyEnv: 'EXA_API_KEY', + needsKey: true, + }, + perplexity: { + transport: 'http', + url: 'https://api.perplexity.ai/mcp', + authStyle: 'header', + authParam: 'Authorization', + authPrefix: 'Bearer ', + toolName: 'perplexity_search', + countArg: 'max_results', + minResults: 1, + maxResultsLimit: 20, + apiKeyEnv: 'PERPLEXITY_API_KEY', + needsKey: true, + }, + duckduckgo: { + transport: 'stdio', + command: 'npx', + args: ['-y', 'duckduckgo-mcp-server@0.1.2'], + authStyle: 'env', + authParam: '', + toolName: 'duckduckgo_web_search', + countArg: 'count', + minResults: 1, + maxResultsLimit: 20, + needsKey: false, + }, + custom: { + transport: 'http', + url: '', + authStyle: 'query', + authParam: '', + authPrefix: '', + toolName: '', + countArg: '', + needsKey: false, + }, +}; + +/** The provider ids offered in the settings UI. */ +export const KNOWN_KINDS = Object.keys(SEARCH_MCP_CATALOG); + +export function clampSearchResults(server, value) { + const minimum = server.minResults ?? 1; + const maximum = server.maxResultsLimit ?? value; + return Math.min(maximum, Math.max(minimum, value)); +} + +/** Resolve a stored entry without allowing known-provider connection overrides. */ +export function resolveServer(server) { + const kind = typeof server?.kind === 'string' && Object.hasOwn(SEARCH_MCP_CATALOG, server.kind) + ? server.kind + : 'custom'; + const preset = SEARCH_MCP_CATALOG[kind]; + if (kind !== 'custom') { + return { + id: typeof server.id === 'string' ? server.id : '', + kind, + apiKey: typeof server.apiKey === 'string' ? server.apiKey : undefined, + apiKeyEnv: typeof server.apiKeyEnv === 'string' ? server.apiKeyEnv : '', + maxResults: server.maxResults, + transport: preset.transport, + url: preset.url ?? '', + command: preset.command ?? '', + args: [...(preset.args ?? [])], + authStyle: preset.authStyle ?? '', + authParam: preset.authParam ?? '', + authPrefix: preset.authPrefix ?? '', + toolName: preset.toolName ?? '', + countArg: preset.countArg ?? '', + minResults: preset.minResults, + maxResultsLimit: preset.maxResultsLimit, + needsKey: preset.needsKey ?? false, + }; + } + + return { + id: typeof server.id === 'string' ? server.id : '', + kind: 'custom', + apiKey: typeof server.apiKey === 'string' ? server.apiKey : undefined, + apiKeyEnv: typeof server.apiKeyEnv === 'string' ? server.apiKeyEnv : '', + maxResults: server.maxResults, + transport: server.transport || preset.transport || 'http', + url: server.url || preset.url || '', + command: server.command || preset.command || '', + args: Array.isArray(server.args) ? [...server.args] : [...(preset.args ?? [])], + authStyle: server.authStyle || preset.authStyle || '', + authParam: server.authParam || preset.authParam || '', + authPrefix: server.authPrefix || preset.authPrefix || '', + toolName: server.toolName || preset.toolName || '', + countArg: preset.countArg || '', + minResults: preset.minResults, + maxResultsLimit: preset.maxResultsLimit, + needsKey: preset.needsKey ?? false, + }; +} diff --git a/lib/client.browser.js b/lib/client.browser.js new file mode 100644 index 0000000..56aa9d1 --- /dev/null +++ b/lib/client.browser.js @@ -0,0 +1,1077 @@ +/* + * dsh-search-mcp — browser half. + * + * Registers one card into the Settings → Plugins → 插件配置 (configurable) tab, + * bound to the `search-mcp` settings namespace registered by the host half. + * + * The card is a structured form (no raw JSON editing), designed to feel like + * the neighbor cards: + * - 默认服务器: dropdown of the configured server ids (friendly labels) + * - 结果数上限 / 搜索超时: numeric inputs (timeout in seconds) + * - 常用提供商 quick-add: one-click preset rows (Tavily / Brave / Exa / + * Perplexity / DuckDuckGo) — only the API key still needs filling + * - 服务器列表: collapsible rows; known providers show only identity, + * provider, write-only key/reference status, and result limit; custom rows + * additionally expose transport, URL/command, authentication, and tool name. + * + * Loaded through window.__ModuleLoader__ like every shipped client bundle. + */ +window.__ModuleLoader__.load({ + id: "dsh-search-mcp", + factory: (require) => { + var module = { exports: {} }; + var exports = module.exports; + Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); + let react_jsx_runtime = require("react/jsx-runtime"); + let react = require("react"); + let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); + // 0.1.2+: createSnapshotStore lives on the platform seed @deepseek-ai/dsh-client-store + // (old @deepseek-ai/dsh-client-runtime/client is gone from the module table). + let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-store"); + + //#region styles + const css = [ + ".smcp_card{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}", + ".smcp_card:hover{border-color:var(--dsw-alias-label-dimmed)}", + ".smcp_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}", + ".smcp_headBtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}", + ".smcp_headBtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}", + ".smcp_head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}", + ".smcp_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}", + ".smcp_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}", + ".smcp_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}", + ".smcp_chevronOpen{transform:rotate(180deg)}", + ".smcp_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:0}", + ".smcp_field{display:flex;flex-direction:column;gap:6px;padding:12px 0}", + ".smcp_field + .smcp_field{border-top:1px solid var(--dsw-alias-border-l2)}", + ".smcp_label{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}", + ".smcp_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}", + ".smcp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}", + ".smcp_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}", + ".smcp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px;line-height:1.5}", + ".smcp_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}", + ".smcp_invalid{border-color:var(--dsw-alias-label-error)}", + ".smcp_error{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}", + ".smcp_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}", + ".smcp_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}", + ".smcp_badgeDanger{white-space:nowrap;color:var(--dsw-alias-label-error);border:1px solid var(--dsw-alias-label-error);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}", + ".smcp_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}", + ".smcp_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}", + ".smcp_reset:disabled{cursor:default}", + ".smcp_footer{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}", + ".smcp_btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 12px;height:auto;min-height:32px;font-size:13px;line-height:1.5}", + /* Match system plugin cards (e.g. dsh-ops-cron): primary text on contrast surface */ + ".smcp_btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}", + ".smcp_btnPrimary:disabled{opacity:.5;cursor:default}", + ".smcp_btnGhost{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary)}", + ".smcp_btnGhost:disabled{opacity:.5;cursor:default}", + ".smcp_failDetail{color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.45;text-align:left}", + ".smcp_quickAdd{display:flex;flex-wrap:wrap;gap:8px;padding:2px 0 6px}", + ".smcp_quickBtn{border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:4px 12px;font-size:12px;line-height:1.5;cursor:pointer}", + ".smcp_quickBtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}", + ".smcp_quickBtn:disabled{opacity:.5;cursor:default}", + ".smcp_row{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;background:var(--dsw-alias-bg-layer-2)}", + ".smcp_rowHead{display:flex;align-items:center;gap:8px;min-width:0}", + ".smcp_rowToggle{appearance:none;border:0;background:0 0;padding:2px;cursor:pointer;color:var(--dsw-alias-label-tertiary);display:inline-flex;align-items:center;flex:none;transition:transform .16s}", + ".smcp_rowToggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px;border-radius:6px}", + ".smcp_rowToggleOpen{transform:rotate(180deg)}", + ".smcp_kindBadge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;flex:none}", + ".smcp_rowTitle{flex:0 0 auto;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}", + ".smcp_rowSummary{flex:1;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", + ".smcp_rowBody{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:2px;padding-top:8px;display:flex;flex-direction:column;gap:8px}", + ".smcp_rowId{flex:1;min-width:0}", + ".smcp_rowGrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}", + ".smcp_rowGrid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}", + ".smcp_cell{display:flex;flex-direction:column;gap:4px;min-width:0}", + ".smcp_cellLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.5}", + ".smcp_cellHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}", + ".smcp_add{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;border:1px dashed var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:8px;padding:6px 12px;font-size:12px;line-height:1.5;cursor:pointer}", + ".smcp_add:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}", + ".smcp_del{border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:8px;padding:4px 10px;font-size:12px;line-height:1.5;cursor:pointer;flex:none}", + ".smcp_del:hover{color:var(--dsw-alias-label-error);border-color:var(--dsw-alias-label-error)}" + ].join(""); + const cssTag = "dsh-search-mcp/card.css"; + if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${cssTag}"]`) === null) { + const tag = document.createElement("style"); + tag.dataset.plugin = "dsh-search-mcp"; + tag.dataset.pluginCss = cssTag; + tag.textContent = css; + document.head.appendChild(tag); + } + //#endregion + + //#region provider catalog (browser-side metadata only) + const CATALOG = { + tavily: { needsKey: true }, + brave: { needsKey: true }, + exa: { needsKey: true }, + perplexity: { needsKey: true }, + duckduckgo: { needsKey: false }, + custom: { needsKey: false } + }; + const KIND_OPTIONS = Object.keys(CATALOG); + const TRANSPORT_OPTIONS = ["http", "stdio"]; + const AUTH_STYLE_OPTIONS = ["", "query", "header"]; + + /** Brand labels shown on the kind badge / quick-add buttons. */ + const KIND_LABELS = { + tavily: "Tavily", + brave: "Brave", + exa: "Exa", + perplexity: "Perplexity", + duckduckgo: "DuckDuckGo", + custom: "Custom" + }; + /** Kinds offered as one-click presets (custom is added via the dashed button). */ + const QUICK_KINDS = ["tavily", "brave", "exa", "perplexity", "duckduckgo"]; + //#endregion + + //#region locale + const en = { + title: "Search MCP", + description: "Search MCP servers behind the web_search tool; the built-in DeepSeek search stays disabled while this plugin is enabled.", + defaultServer: "Default server", + defaultServerHint: "Which server row serves searches (empty = first row).", + maxResults: "Max results", + maxResultsHint: "Sources returned per search (1–50; empty = default).", + searchTimeoutMs: "Search timeout (s)", + searchTimeoutMsHint: "Abort a search that takes longer than this.", + servers: "Search MCP servers", + serversHint: "Choose a provider and enter its CDKey/API key. Known providers keep connection details in the host catalog; custom rows expose advanced MCP settings.", + quickAdd: "Common providers", + quickAddHint: "Add a provider row, then enter its CDKey/API key before saving. No endpoint is required for known providers.", + addServer: "Add custom server", + removeServer: "Delete", + rowId: "ID", + rowIdHint: "Stable id used by “default server”.", + rowKind: "Provider", + rowTransport: "Transport", + rowUrl: "Endpoint (URL)", + rowCommand: "Command", + rowArgs: "Args (comma separated)", + rowApiKey: "API key", + cdKey: "CDKey / API key", + cdKeyHint: "enter CDKey / API key", + keyNotRequired: "no key required", + + rowApiKeyEnv: "Key env/credential ref", + rowAuthStyle: "Key placement", + rowAuthParam: "Key param / env name", + rowToolName: "MCP tool name", + rowMaxResults: "Max results", + rowMaxResultsHint: "Empty = follow the global max results.", + missingId: "ID is required.", + dupId: "This ID is already used by another row.", + overridden: "Overridden", + overridesGlobal: "overrides global", + keySet: "key set", + keyRef: "credential ref", + keyConfigured: "credential configured", + keyRefMissing: "credential missing", + keyMissing: "key missing", + legacyKey: "legacy key", + legacyKeyBlocked: "This server still uses a hidden legacy literal key. Enter a replacement key or credential reference before changing the server list.", + + customKind: "Custom", + reset: "Reset to default", + readOnly: "This deployment stores settings read-only.", + save: "Save", + saving: "Saving…", + discard: "Discard", + unsaved: "Unsaved", + saveFailed: "The deployment did not accept these values; they were left for you to correct.", + placeholderUrl: "https://mcp.example.com/mcp/", + placeholderCommand: "npx", + placeholderArgs: "-y, duckduckgo-mcp-server", + placeholderToolName: "tavily_search", + placeholderAuthParam: "tavilyApiKey" + }; + const zh = { + title: "搜索 MCP", + description: "web_search 工具背后的搜索 MCP 服务器;插件启用期间内置 DeepSeek 搜索保持禁用。", + defaultServer: "默认服务器", + defaultServerHint: "使用哪一行服务器(留空 = 第一行)。", + maxResults: "结果数上限", + maxResultsHint: "每次搜索最多返回的条数(1–50;留空 = 默认)。", + searchTimeoutMs: "搜索超时(秒)", + searchTimeoutMsHint: "超过该时长即中止搜索。", + servers: "搜索 MCP 服务器", + serversHint: "选择提供商并填写 CDKey/API key。已知提供商的连接细节由 Host catalog 管理;自定义行才显示 MCP 高级设置。", + quickAdd: "常用提供商", + quickAddHint: "添加提供商行后填写 CDKey/API key 再保存。已知提供商不需要填写端点链接。", + addServer: "添加自定义服务器", + removeServer: "删除", + rowId: "ID", + rowIdHint: "“默认服务器”下拉框引用的稳定标识。", + rowKind: "提供商", + rowTransport: "传输方式", + rowUrl: "端点(URL)", + rowCommand: "命令", + rowArgs: "参数(逗号分隔)", + rowApiKey: "API 密钥", + cdKey: "CDKey / API key", + cdKeyHint: "请填写 CDKey / API key", + keyNotRequired: "无需密钥", + + rowApiKeyEnv: "密钥环境变量/凭证引用", + rowAuthStyle: "密钥位置", + rowAuthParam: "密钥参数名/环境变量名", + rowToolName: "MCP 工具名", + rowMaxResults: "结果数上限", + rowMaxResultsHint: "留空 = 跟随全局结果数上限。", + missingId: "ID 不能为空。", + dupId: "该 ID 已被另一行占用。", + overridden: "已覆盖", + overridesGlobal: "覆盖全局", + keySet: "已填密钥", + keyRef: "凭证引用", + keyConfigured: "凭证已配置", + keyRefMissing: "凭证未配置", + keyMissing: "缺少密钥", + legacyKey: "旧版密钥", + legacyKeyBlocked: "此服务器仍使用客户端不可见的旧版字面密钥。修改服务器列表前,请输入替代密钥或凭证引用。", + + customKind: "自定义", + reset: "恢复默认", + readOnly: "本部署的设置为只读。", + save: "保存", + saving: "保存中…", + discard: "放弃修改", + unsaved: "未保存", + saveFailed: "本部署没有接受这些值,已保留供你修改。", + placeholderUrl: "https://mcp.example.com/mcp/", + placeholderCommand: "npx", + placeholderArgs: "-y, duckduckgo-mcp-server", + placeholderToolName: "tavily_search", + placeholderAuthParam: "tavilyApiKey" + }; + //#endregion + + /** Namespace of this plugin's settings section (spelled, not imported). */ + const NS = "search-mcp"; + + //#region row helpers + function emptyRow() { + return { + id: "", + kind: "custom", + transport: "http", + url: "", + command: "", + args: "", + apiKey: "", + apiKeyEnv: "", + authStyle: "", + authParam: "", + authPrefix: "", + toolName: "", + maxResults: "" + }; + } + /** Convert a stored server entry (host shape) to a row draft. */ + function rowFromEntry(entry) { + const kind = Object.hasOwn(CATALOG, entry.kind) ? entry.kind : "custom"; + const known = kind !== "custom"; + return { + id: entry.id ?? "", + kind, + transport: known ? "" : (entry.transport ?? "http"), + url: known ? "" : (entry.url ?? ""), + command: known ? "" : (entry.command ?? ""), + args: known ? "" : (Array.isArray(entry.args) ? entry.args.join(", ") : ""), + apiKey: entry.apiKey ?? "", + apiKeyEnv: entry.apiKeyEnv ?? "", + authStyle: known ? "" : (entry.authStyle ?? ""), + authParam: known ? "" : (entry.authParam ?? ""), + authPrefix: known ? "" : (entry.authPrefix ?? ""), + toolName: known ? "" : (entry.toolName ?? ""), + maxResults: entry.maxResults === undefined ? "" : String(entry.maxResults) + }; + } + /** Convert a row draft to the minimal known entry or full custom entry. */ + function entryFromRow(row) { + const entry = { id: row.id.trim(), kind: row.kind }; + const known = Object.hasOwn(CATALOG, row.kind) && row.kind !== "custom"; + if (!known) { + entry.transport = row.transport || "http"; + if (entry.transport === "stdio") { + if (row.command.trim() !== "") entry.command = row.command.trim(); + const args = row.args.split(",").map((a) => a.trim()).filter(Boolean); + if (args.length > 0) entry.args = args; + } else { + const url = normalizeServerUrl(row); + if (url !== "") entry.url = url; + } + if (row.authStyle !== "") entry.authStyle = row.authStyle; + if (row.authParam.trim() !== "") entry.authParam = row.authParam.trim(); + const prefix = resolveAuthPrefix(row); + if (prefix !== "") entry.authPrefix = prefix; + if (row.toolName.trim() !== "") entry.toolName = row.toolName.trim(); + } + if (row.apiKey.trim() !== "") entry.apiKey = row.apiKey.trim(); + if (row.apiKeyEnv.trim() !== "") entry.apiKeyEnv = row.apiKeyEnv.trim(); + if (row.maxResults.trim() !== "") { + const n = Number(row.maxResults); + if (Number.isFinite(n) && n > 0) entry.maxResults = Math.round(n); + } + return entry; + } + /** Changing provider starts a clean credential/connection draft. */ + function applyKindDefaults(row, kind) { + const next = { ...row, kind, apiKey: "", apiKeyEnv: "" }; + if (kind !== "custom") { + return { ...next, transport: "", url: "", command: "", args: "", authStyle: "", authParam: "", toolName: "" }; + } + return { ...next, transport: row.transport || "http" }; + } + + /** Suggest a unique row id for a preset kind (tavily, tavily2, …). */ + function suggestId(servers, kind) { + const used = new Set(servers.map((row) => row.id.trim()).filter(Boolean)); + let id = kind; + let n = 2; + while (used.has(id)) id = `${kind}${n++}`; + return id; + } + /** Brand label for a kind, localized for `custom`. */ + function kindLabel(kind, t) { + if (kind === "custom") return t("customKind"); + return KIND_LABELS[kind] ?? kind; + } + function deepEqualJson(left, right) { + return JSON.stringify(left) === JSON.stringify(right); + } + /** Snapshot-safe copy — `createSnapshotStore.set` deep-freezes state in + * non-production, so the live editable draft must never be published by reference. */ + function cloneDraft(draft) { + return { + defaultServer: draft.defaultServer, + maxResults: draft.maxResults, + searchTimeoutMs: draft.searchTimeoutMs, + servers: draft.servers.map((row) => ({ ...row })), + }; + } + function credentialRefFor(row) { + if (row.apiKeyEnv.trim() !== "") return row.apiKeyEnv.trim(); + const url = (row.url || "").toLowerCase(); + if (url.includes("dashscope.aliyuncs.com")) return "DASHSCOPE_API_KEY"; + const id = row.id.trim().replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase(); + return `SEARCH_MCP_${id || "SERVER"}_API_KEY`; + } + /** Normalize Bailian WebSearch endpoint if the user truncated it. */ + function normalizeServerUrl(row) { + let url = (row.url || "").trim(); + if (!url) return url; + if (row.toolName === "bailian_web_search" || url.includes("dashscope.aliyuncs.com")) { + if (url === "https://dashscope.aliyuncs.com/api/v1" + || url === "https://dashscope.aliyuncs.com/api/v1/" + || /\/api\/v1\/?$/.test(url) && url.includes("dashscope")) { + url = "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp"; + } + if (url.endsWith("/WebSearch/sse")) { + url = url.replace(/\/WebSearch\/sse$/, "/WebSearch/mcp"); + } + } + return url; + } + function resolveAuthPrefix(row) { + const explicit = (row.authPrefix || "").trim(); + if (explicit !== "") return explicit.endsWith(" ") ? explicit : `${explicit} `; + if (row.authStyle === "header" && row.authParam.trim() === "Authorization") return "Bearer "; + return ""; + } + const CREDENTIAL_DESCRIBE_BATCH_SIZE = 64; + /** 0.1.2+ Remote face: `remote.credentials.describe(refs)` → `{ ok, value }`. */ + async function describeCredentialRefs(remote, refs) { + const states = {}; + for (let index = 0; index < refs.length; index += CREDENTIAL_DESCRIBE_BATCH_SIZE) { + const batch = refs.slice(index, index + CREDENTIAL_DESCRIBE_BATCH_SIZE); + const response = await remote.credentials.describe(batch); + if (!response.ok) throw new Error(response.error?.message || "credential state lookup failed"); + Object.assign(states, response.value ?? {}); + } + return states; + } + async function rollbackNewCredentialRefs(remote, refs) { + for (const ref of [...new Set(refs)].reverse()) { + try { + await remote.credentials.unset(ref); + } catch { + // The save already failed; preserve the original error state. + } + } + } + async function rollbackSettingsWrites(scope, writes) { + for (const [field, previous] of writes.reverse()) { + try { + if (previous === undefined) await scope.unset(field); + else await scope.set(field, previous); + } catch { + // The UI remains failed so the user can retry or discard the draft. + } + } + } + //#endregion + + + //#region controller (whole-section staged draft) + var SearchMcpCardController = class { + constructor(scope, remote, describeFace) { + this.scope = scope; + this.remote = remote; + this.describeFace = describeFace; + this.draft = null; + this.listeners = new Set(); + this.saving = false; + this.failed = false; + this.failDetail = ""; + this.legacyBlocked = false; + this.secretStateReady = false; + this.legacySecretIds = new Set(); + this.credentialStates = {}; + this.store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.project()); + scope.subscribe(() => { + if (this.draft === null) this.publish(); + this.readSecretState(); + }); + this.readSecretState(); + } + section() { + return this.scope.getSnapshot(); + } + value() { + return this.section().value ?? {}; + } + userLayer() { + return this.section().user; + } + baseDraft() { + const value = this.value(); + return { + defaultServer: typeof value.defaultServer === "string" ? value.defaultServer : "", + maxResults: typeof value.maxResults === "number" ? String(value.maxResults) : "", + searchTimeoutMs: typeof value.searchTimeoutMs === "number" ? String(value.searchTimeoutMs) : "", + servers: Array.isArray(value.servers) ? value.servers.map((entry) => ({ + ...rowFromEntry(entry), + legacySecret: this.legacySecretIds.has(entry.id) + })) : [] + }; + } + async readSecretState() { + let view; + try { + const response = await this.remote.settings.describe(); + if (!response.ok) { + this.secretStateReady = true; + this.publish(); + return; + } + view = (response.value?.namespaces ?? []).find((candidate) => candidate.ns === NS); + } catch { + this.secretStateReady = true; + this.publish(); + return; + } + const servers = Array.isArray(view?.value?.servers) ? view.value.servers : []; + const ids = new Set(); + for (const secret of view?.secrets ?? []) { + if (!secret.set || secret.path?.[0] !== "servers" || secret.path?.[2] !== "apiKey") continue; + const index = Number(secret.path[1]); + const id = Number.isInteger(index) ? servers[index]?.id : undefined; + if (typeof id === "string" && id.length > 0) ids.add(id); + } + const wasReady = this.secretStateReady; + const changed = !deepEqualJson([...ids].sort(), [...this.legacySecretIds].sort()); + this.legacySecretIds = ids; + const previousCredentialStates = this.credentialStates; + const refs = [...new Set(servers.map((entry) => typeof entry.apiKeyEnv === "string" ? entry.apiKeyEnv.trim() : "").filter(Boolean))]; + if (refs.length > 0) { + try { + this.credentialStates = await describeCredentialRefs(this.remote, refs); + } catch { + this.credentialStates = {}; + } + } else { + this.credentialStates = {}; + } + + this.secretStateReady = true; + const credentialsChanged = !deepEqualJson(previousCredentialStates, this.credentialStates); + if (this.draft === null && (changed || credentialsChanged || !wasReady)) this.publish(); + } + + + ensureDraft() { + if (this.draft === null) this.draft = this.baseDraft(); + return this.draft; + } + project() { + const snapshot = this.section(); + const draft = this.draft; + const sectionValue = snapshot.value ?? {}; + const dirty = draft !== null && JSON.stringify(draft) !== JSON.stringify(this.baseDraft()); + const ids = draft !== null ? draft.servers.map((row) => row.id.trim()) : []; + const invalid = draft !== null && (ids.some((id) => id === "") || new Set(ids.filter(Boolean)).size !== ids.filter(Boolean).length); + const serverKinds = draft !== null + ? Object.fromEntries(draft.servers.map((row) => [row.id.trim(), row.kind])) + : Object.fromEntries((Array.isArray(sectionValue.servers) ? sectionValue.servers : []).map((s) => [s.id, s.kind])); + return { + available: snapshot.status === "ready", + // Do not gate edits on secretStateReady — a failed describe must not freeze the form. + writable: snapshot.writable === true, + + dirty, + invalid, + saving: this.saving, + failed: this.failed, + failDetail: this.failDetail, + legacyBlocked: this.legacyBlocked, + + overridden: this.userLayer() !== null && typeof this.userLayer() === "object" && !Array.isArray(this.userLayer()) + ? Object.keys(this.userLayer()).length > 0 + : false, + value: draft !== null ? cloneDraft(draft) : this.baseDraft(), + serverIds: draft !== null + ? ids.filter(Boolean) + : (Array.isArray(sectionValue.servers) ? sectionValue.servers.map((s) => s.id).filter(Boolean) : []), + serverKinds, + credentialStates: this.credentialStates + + }; + } + publish() { + this.store.set(this.project()); + } + actions() { + return { + editScalar: (field, text) => { + this.ensureDraft()[field] = text; + this.failed = false; + this.legacyBlocked = false; + this.publish(); + + }, + editRow: (index, field, value) => { + const draft = this.ensureDraft(); + draft.servers[index] = { ...draft.servers[index], [field]: value }; + this.failed = false; + this.legacyBlocked = false; + this.publish(); + + }, + changeKind: (index, kind) => { + const draft = this.ensureDraft(); + draft.servers[index] = applyKindDefaults(draft.servers[index], kind); + this.failed = false; + this.legacyBlocked = false; + + this.publish(); + }, + addServer: () => { + const draft = this.ensureDraft(); + draft.servers.push(emptyRow()); + this.failed = false; + this.legacyBlocked = false; + + this.publish(); + }, + quickAdd: (kind) => { + const draft = this.ensureDraft(); + const id = suggestId(draft.servers, kind); + const row = applyKindDefaults({ ...emptyRow(), id, kind }, kind); + draft.servers.push(row); + this.failed = false; + this.legacyBlocked = false; + + this.publish(); + }, + removeServer: (index) => { + const draft = this.ensureDraft(); + draft.servers.splice(index, 1); + this.failed = false; + this.legacyBlocked = false; + + this.publish(); + }, + discard: () => { + if (this.draft === null && !this.failed && !this.legacyBlocked) return; + this.draft = null; + this.failed = false; + this.failDetail = ""; + this.legacyBlocked = false; + this.publish(); + + }, + save: () => { + this.save(); + } + }; + } + async save() { + if (this.draft === null || this.saving) return; + + const draft = this.draft; + const ids = draft.servers.map((row) => row.id.trim()); + const invalid = ids.some((id) => id === "") || new Set(ids.filter(Boolean)).size !== ids.filter(Boolean).length; + if (invalid) return; + const base = this.baseDraft(); + const serversChanged = !deepEqualJson(draft.servers, base.servers); + if (serversChanged) { + const blocked = draft.servers.some((row) => row.legacySecret && row.apiKey.trim() === "" && row.apiKeyEnv.trim() === ""); + if (blocked) { + this.legacyBlocked = true; + this.publish(); + return; + } + } + this.saving = true; + this.failed = false; + this.failDetail = ""; + this.legacyBlocked = false; + this.publish(); + + let landed = true; + let failDetail = ""; + let serverEntries; + const createdCredentialRefs = []; + if (serversChanged) { + serverEntries = []; + const credentialWrites = draft.servers + .filter((row) => row.apiKey.trim() !== "") + .map((row) => ({ ref: credentialRefFor(row), value: row.apiKey.trim() })); + let credentialBefore = {}; + try { + if (credentialWrites.length > 0) { + credentialBefore = await describeCredentialRefs(this.remote, [...new Set(credentialWrites.map(({ ref }) => ref))]); + } + } catch (error) { + landed = false; + failDetail = error instanceof Error ? error.message : String(error); + } + if (landed && credentialWrites.some(({ ref }) => credentialBefore[ref]?.writable === false)) { + landed = false; + failDetail = "credential is not writable"; + } + if (landed) { + for (const { ref, value } of credentialWrites) { + if (!credentialBefore[ref]?.configured) createdCredentialRefs.push(ref); + try { + const response = await this.remote.credentials.set(ref, value); + if (!response.ok) { + landed = false; + failDetail = response.error?.message || "credentials.set failed"; + break; + } + } catch (error) { + landed = false; + failDetail = error instanceof Error ? error.message : String(error); + break; + } + } + } + if (!landed) { + await rollbackNewCredentialRefs(this.remote, createdCredentialRefs); + this.saving = false; + this.failed = true; + this.failDetail = failDetail; + this.publish(); + return; + } + for (const row of draft.servers) { + const entry = entryFromRow(row); + if (row.apiKey.trim() !== "") { + delete entry.apiKey; + entry.apiKeyEnv = credentialRefFor(row); + } else if (!entry.apiKeyEnv && (row.url || "").includes("dashscope")) { + entry.apiKeyEnv = "DASHSCOPE_API_KEY"; + } + delete entry.legacySecret; + serverEntries.push(entry); + } + } + + const ops = []; + const planned = [ + ["defaultServer", draft.defaultServer.trim(), base.defaultServer], + ["maxResults", draft.maxResults.trim() === "" ? undefined : Number(draft.maxResults), base.maxResults === "" ? undefined : Number(base.maxResults)], + ["searchTimeoutMs", draft.searchTimeoutMs.trim() === "" ? undefined : Number(draft.searchTimeoutMs), base.searchTimeoutMs === "" ? undefined : Number(base.searchTimeoutMs)], + ...(serversChanged ? [["servers", serverEntries, this.value().servers]] : []) + ]; + for (const [field, value, previous] of planned) { + if (deepEqualJson(value, previous)) continue; + if (value === undefined) ops.push({ op: "unset", path: [field] }); + else ops.push({ op: "set", path: [field], value }); + } + + if (ops.length > 0) { + try { + const revision = this.section().revision; + const response = await this.remote.settings.mutate(NS, ops, revision); + if (!response.ok) { + landed = false; + failDetail = response.error?.message || response.error?.code || "settings.mutate failed"; + try { + if (typeof this.describeFace?.load === "function") await this.describeFace.load(); + else if (typeof this.describeFace?.ensure === "function") await this.describeFace.ensure(); + } catch { /* keep draft */ } + } else if (response.value && typeof this.describeFace?.acceptView === "function") { + this.describeFace.acceptView(response.value); + } + } catch (error) { + landed = false; + failDetail = error instanceof Error ? error.message : String(error); + } + } + + if (!landed) { + await rollbackNewCredentialRefs(this.remote, createdCredentialRefs); + } else { + this.draft = null; + } + this.saving = false; + this.failed = !landed; + this.failDetail = landed ? "" : failDetail; + this.publish(); + if (landed) this.readSecretState(); + } + + inject() { + return { + hooks: { searchMcpCard: this.store }, + ...this.actions() + }; + } + }; + //#endregion + + //#region components + function SearchMcpCard(props) { + const { t } = props; + const [open, setOpen] = (0, react.useState)(false); + const state = props.useSearchMcpCard((snapshot) => snapshot); + if (!state.available) return null; + const disabled = !state.writable; + const value = state.value; + const title = t("title"); + const serverOptions = [ + { value: "", label: "—" }, + ...state.serverIds.map((id) => ({ value: id, label: `${id} · ${kindLabel(state.serverKinds[id], t)}` })) + ]; + const rowIds = value.servers.map((row) => row.id.trim()).filter(Boolean); + const dupIds = new Set(rowIds.filter((id, index) => rowIds.indexOf(id) !== index)); + return (0, react_jsx_runtime.jsxs)("li", { + className: "smcp_card" + (open ? " smcp_cardOpen" : ""), + children: [ + (0, react_jsx_runtime.jsxs)("button", { + type: "button", + className: "smcp_headBtn", + "aria-expanded": open, + "aria-label": `${open ? "收起" : "展开"}: ${title}`, + onClick: () => { + setOpen(!open); + }, + children: [ + (0, react_jsx_runtime.jsxs)("span", { + className: "smcp_head", + children: [ + (0, react_jsx_runtime.jsx)("span", { className: "smcp_name", children: title }), + (0, react_jsx_runtime.jsx)("span", { className: "smcp_desc", children: t("description") }), + state.overridden ? (0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: t("overridden") }) : null, + state.dirty ? (0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("unsaved") }) : null + ] + }), + (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { + className: "smcp_chevron" + (open ? " smcp_chevronOpen" : "") + }) + ] + }), + open ? (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_body", + children: [ + !state.writable ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("readOnly") }) : null, + (0, react_jsx_runtime.jsx)(SelectField, { + id: "smcp-default-server", + label: t("defaultServer"), + hint: t("defaultServerHint"), + disabled, + options: serverOptions, + value: value.defaultServer, + onEdit: (text) => props.editScalar("defaultServer", text) + }), + (0, react_jsx_runtime.jsx)(TextField, { + id: "smcp-max-results", + label: t("maxResults"), + hint: t("maxResultsHint"), + numeric: true, + disabled, + value: value.maxResults, + onEdit: (text) => props.editScalar("maxResults", text) + }), + (0, react_jsx_runtime.jsx)(TextField, { + id: "smcp-timeout", + label: t("searchTimeoutMs"), + hint: t("searchTimeoutMsHint"), + numeric: true, + disabled, + value: value.searchTimeoutMs === "" ? "" : String(Math.round(Number(value.searchTimeoutMs) / 1000)), + onEdit: (text) => props.editScalar("searchTimeoutMs", text === "" ? "" : String(Math.round((Number(text) || 0) * 1000))) + }), + (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_field", + children: [ + (0, react_jsx_runtime.jsx)("span", { className: "smcp_label", children: t("servers") }), + (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("serversHint") }), + (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("quickAddHint") }), + (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_quickAdd", + children: QUICK_KINDS.map((kind) => (0, react_jsx_runtime.jsx)( + "button", + { + type: "button", + className: "smcp_quickBtn", + disabled, + onClick: () => props.quickAdd(kind), + children: `+ ${KIND_LABELS[kind]}` + }, + `smcp-quick-${kind}` + )) + }), + value.servers.map((row, index) => (0, react_jsx_runtime.jsx)( + ServerRow, + { + t, + row, + index, + disabled, + dupIds, + credentialStates: state.credentialStates, + + onEdit: (field, val) => props.editRow(index, field, val), + onKind: (kind) => props.changeKind(index, kind), + onRemove: () => props.removeServer(index) + }, + `smcp-row-${index}` + )), + (0, react_jsx_runtime.jsx)("button", { + type: "button", + className: "smcp_add", + disabled, + onClick: props.addServer, + children: `+ ${t("addServer")}` + }) + ] + }), + state.legacyBlocked ? (0, react_jsx_runtime.jsx)("p", { role: "status", className: "smcp_error", children: t("legacyKeyBlocked") }) : null, + + (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_footer", + children: [ + state.failed ? (0, react_jsx_runtime.jsx)("p", { role: "status", className: "smcp_failDetail", children: state.failDetail ? (t("saveFailed") + " — " + state.failDetail) : t("saveFailed") }) : null, + (0, react_jsx_runtime.jsx)("button", { + type: "button", + className: "smcp_btn smcp_btnGhost", + disabled: !state.dirty || state.saving, + onClick: props.discard, + children: t("discard") + }), + (0, react_jsx_runtime.jsx)("button", { + type: "button", + className: "smcp_btn smcp_btnPrimary", + disabled: !state.dirty || state.invalid || state.saving, + onClick: props.save, + children: t(state.saving ? "saving" : "save") + }) + ] + }) + ] + }) : null + ] + }); + } + function ServerRow(props) { + const { t, row, index, disabled, onEdit, onKind, onRemove } = props; + const [rowOpen, setRowOpen] = (0, react.useState)(false); + const rowInvalid = row.id.trim() === ""; + const dup = !rowInvalid && row.id.trim() !== "" && props.dupIds && props.dupIds.has(row.id.trim()); + const known = row.kind !== "custom"; + const needsKey = (CATALOG[row.kind] ?? CATALOG.custom).needsKey; + const hasKey = row.apiKey.trim() !== ""; + const hasKeyEnv = row.apiKeyEnv.trim() !== ""; + const hasConfiguredRef = hasKeyEnv && props.credentialStates?.[row.apiKeyEnv.trim()]?.configured === true; + const hasLegacyKey = row.legacySecret === true; + const overridesGlobal = row.maxResults.trim() !== ""; + const summary = known ? (needsKey ? (hasKeyEnv ? `${t("keyRef")}: ${row.apiKeyEnv.trim()}` : t("cdKeyHint")) : t("keyNotRequired")) : (row.transport === "stdio" + ? ((row.command.trim() !== "" ? row.command.trim() : "") + (row.args.trim() !== "" ? " " + row.args.trim() : "")).trim() || row.id + : (row.url.trim() !== "" ? row.url.trim() : row.id)); + const badges = []; + if (hasKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: t("keySet") }, "badge-key")); + else if (hasConfiguredRef) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: `${t("keyConfigured")}: ${row.apiKeyEnv.trim()}` }, "badge-env")); + else if (hasKeyEnv) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeDanger", children: `${t("keyRefMissing")}: ${row.apiKeyEnv.trim()}` }, "badge-env-missing")); + else if (hasLegacyKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("legacyKey") }, "badge-legacy-key")); + else if (needsKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeDanger", children: t("keyMissing") }, "badge-key-missing")); + if (overridesGlobal) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("overridesGlobal") }, "badge-override")); + return (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_row", + children: [ + (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_rowHead", + children: [ + (0, react_jsx_runtime.jsx)("button", { + type: "button", + className: "smcp_rowToggle" + (rowOpen ? " smcp_rowToggleOpen" : ""), + "aria-expanded": rowOpen, + "aria-label": `${rowOpen ? "收起" : "展开"} ${row.id || t("rowId")}`, + disabled, + onClick: () => setRowOpen(!rowOpen), + children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {}) + }), + (0, react_jsx_runtime.jsx)("span", { className: "smcp_kindBadge", children: kindLabel(row.kind, t) }), + (0, react_jsx_runtime.jsx)("span", { className: "smcp_rowTitle" + (rowInvalid ? " smcp_invalid" : ""), children: row.id.trim() !== "" ? row.id.trim() : t("rowId") }), + (0, react_jsx_runtime.jsx)("span", { className: "smcp_rowSummary", children: summary }), + ...badges, + (0, react_jsx_runtime.jsx)("button", { type: "button", className: "smcp_del", disabled, onClick: onRemove, children: t("removeServer") }) + ] + }), + rowInvalid ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_error", children: t("missingId") }) : null, + dup ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_error", children: t("dupId") }) : null, + rowOpen ? (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_rowBody", + children: known ? [ + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowId"), placeholder: t("rowIdHint"), value: row.id, disabled, onEdit: (v) => onEdit("id", v) }), + (0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid3", children: [ + (0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowKind"), options: KIND_OPTIONS, value: row.kind, disabled, onEdit: onKind }), + needsKey ? (0, react_jsx_runtime.jsx)(CellInput, { label: t("cdKey"), type: "password", placeholder: t("cdKeyHint"), value: row.apiKey, disabled, onEdit: (v) => onEdit("apiKey", v) }) : null, + needsKey ? (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKeyEnv"), placeholder: t("rowApiKeyEnv"), value: row.apiKeyEnv, disabled, onEdit: (v) => onEdit("apiKeyEnv", v) }) : null + ] }), + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowMaxResults"), placeholder: t("rowMaxResultsHint"), value: row.maxResults, disabled, onEdit: (v) => onEdit("maxResults", v) }) + ] : [ + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowId"), placeholder: t("rowIdHint"), value: row.id, disabled, onEdit: (v) => onEdit("id", v) }), + (0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid3", children: [ + (0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowKind"), options: KIND_OPTIONS, value: row.kind, disabled, onEdit: onKind }), + (0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowTransport"), options: TRANSPORT_OPTIONS, value: row.transport, disabled, onEdit: (v) => onEdit("transport", v) }), + (0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowAuthStyle"), options: AUTH_STYLE_OPTIONS, value: row.authStyle, disabled, onEdit: (v) => onEdit("authStyle", v) }) + ] }), + (0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [ + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowUrl"), placeholder: t("placeholderUrl"), value: row.url, disabled, onEdit: (v) => onEdit("url", v) }), + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKey"), type: "password", placeholder: t("cdKeyHint"), value: row.apiKey, disabled, onEdit: (v) => onEdit("apiKey", v) }) + ] }), + (0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [ + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKeyEnv"), placeholder: t("rowApiKeyEnv"), value: row.apiKeyEnv, disabled, onEdit: (v) => onEdit("apiKeyEnv", v) }), + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowAuthParam"), placeholder: t("placeholderAuthParam"), value: row.authParam, disabled, onEdit: (v) => onEdit("authParam", v) }) + ] }), + (0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [ + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowCommand"), placeholder: t("placeholderCommand"), value: row.command, disabled, onEdit: (v) => onEdit("command", v) }), + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowArgs"), placeholder: t("placeholderArgs"), value: row.args, disabled, onEdit: (v) => onEdit("args", v) }) + ] }), + (0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [ + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowToolName"), placeholder: t("placeholderToolName"), value: row.toolName, disabled, onEdit: (v) => onEdit("toolName", v) }), + (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowMaxResults"), placeholder: t("rowMaxResultsHint"), value: row.maxResults, disabled, onEdit: (v) => onEdit("maxResults", v) }) + ] }) + ] + }) : null + ] + }); + } + + function CellInput(props) { + return (0, react_jsx_runtime.jsxs)("label", { + className: "smcp_cell", + children: [ + (0, react_jsx_runtime.jsx)("span", { className: "smcp_cellLabel", children: props.label }), + (0, react_jsx_runtime.jsx)("input", { + className: "smcp_input", + type: props.type ?? "text", + placeholder: props.placeholder, + value: props.value, + disabled: props.disabled, + onChange: (e) => props.onEdit(e.target.value) + }) + ] + }); + } + function CellSelect(props) { + return (0, react_jsx_runtime.jsxs)("label", { + className: "smcp_cell", + children: [ + (0, react_jsx_runtime.jsx)("span", { className: "smcp_cellLabel", children: props.label }), + (0, react_jsx_runtime.jsx)("select", { + className: "smcp_select", + value: props.value, + disabled: props.disabled, + onChange: (e) => props.onEdit(e.target.value), + children: props.options.map((option) => (0, react_jsx_runtime.jsx)("option", { value: option, children: option === "" ? "—" : option }, option)) + }) + ] + }); + } + function TextField(props) { + return (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_field", + children: [ + (0, react_jsx_runtime.jsx)("label", { className: "smcp_label", htmlFor: props.id, children: props.label }), + (0, react_jsx_runtime.jsx)("input", { + id: props.id, + className: "smcp_input", + type: "text", + inputMode: props.numeric ? "numeric" : undefined, + value: props.value, + disabled: props.disabled, + onChange: (e) => props.onEdit(e.target.value) + }), + (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: props.hint }) + ] + }); + } + function SelectField(props) { + const options = props.options.map((option) => typeof option === "string" ? { value: option, label: option } : option); + return (0, react_jsx_runtime.jsxs)("div", { + className: "smcp_field", + children: [ + (0, react_jsx_runtime.jsx)("label", { className: "smcp_label", htmlFor: props.id, children: props.label }), + (0, react_jsx_runtime.jsx)("select", { + id: props.id, + className: "smcp_select", + value: props.value, + disabled: props.disabled, + onChange: (e) => props.onEdit(e.target.value), + children: options.map((option) => (0, react_jsx_runtime.jsx)("option", { value: option.value, children: option.label }, option.value)) + }), + (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: props.hint }) + ] + }); + } + //#endregion + + //#region apply + /** Required client services (resolved by the client kernel). */ + const inject = ["slots", "locale", "settingsScope", "remote", "remote.credentials", "remote.settings"]; + + + function apply(ctx) { + const t = ctx.locale.bind(NS); + ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-search-mcp: section dictionaries"); + const describeFace = ctx.settingsScope.describe(); + const controller = new SearchMcpCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.remote, describeFace); + ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => { + const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref); + if (used) controller.readSecretState(); + }), "dsh-search-mcp: credential status invalidation"); + + ctx.slots.inject("settings.plugin.item", function* () { + yield ctx.slots.register({ + name: "settings.plugin.item", + key: NS, + order: 30, + + locale: NS, + inject: () => controller.inject() + }, SearchMcpCard); + }); + } + //#endregion + + exports.apply = apply; + exports.inject = inject; + return module.exports; + } +}); diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..c4dbcb8 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,150 @@ +/** MCP transport layer for one search. */ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { WebError } from '@deepseek-ai/dsh-web'; +import { Agent, fetch as undiciFetch } from 'undici'; +import { clampSearchResults } from './catalog.js'; +import { validateHttpEndpoint } from './url-policy.js'; + +/** Run one search through a resolved server entry. */ +export async function callMcpSearch(server, key, args, signal) { + if (!server.toolName) { + throw new WebError( + `search-mcp server "${server.id}": no MCP tool name (set "toolName" or pick a known kind)`, + 'WEB_PROVIDER_ERROR', + ); + } + + let runtime; + const client = new Client({ name: 'dsh-search-mcp', version: '0.2.0' }, { capabilities: {} }); + try { + runtime = server.transport === 'stdio' + ? { transport: stdioTransport(server, key), close: async () => {} } + : await httpRuntime(server, key, signal); + await race(client.connect(runtime.transport), signal, `connect to "${server.id}"`); + const callArgs = { query: args.query }; + if (server.countArg.length > 0 && args.maxResults !== undefined) { + callArgs[server.countArg] = clampSearchResults(server, args.maxResults); + } + const result = await race( + client.callTool({ name: server.toolName, arguments: callArgs }), + signal, + `call "${server.id}" tool "${server.toolName}"`, + ); + if (result.isError) { + throw new WebError( + `search-mcp: MCP server "${server.id}" tool "${server.toolName}" reported an error`, + 'WEB_PROVIDER_ERROR', + ); + } + return result; + } catch (error) { + if (error instanceof WebError) throw error; + if (signal?.aborted) throw aborted(`complete request for "${server.id}"`); + const detail = error?.name === 'SearchMcpUrlPolicyError' ? `: ${error.message}` : ''; + throw new WebError( + `search-mcp server "${server.id}" request failed${detail}`, + 'WEB_PROVIDER_ERROR', + ); + } finally { + try { + await client.close(); + } catch { + // The connection is already gone. + } + try { + await runtime?.close(); + } catch { + // The dedicated dispatcher has no shared state to recover. + } + } +} + +/** Build a DNS-pinned streamable-http transport and its cleanup. */ +async function httpRuntime(server, key, signal) { + const validated = await validateHttpEndpoint(server.url, { signal }); + const url = new URL(validated.url); + const headers = {}; + if (key !== undefined && key.length > 0 && server.authParam.length > 0) { + const value = `${server.authPrefix ?? ''}${key}`; + if (server.authStyle === 'query') url.searchParams.set(server.authParam, value); + else if (server.authStyle === 'header') headers[server.authParam] = value; + } + + const agent = new Agent({ + connect: { lookup: validated.lookup }, + connections: validated.addresses.length, + maxRedirections: 0, + }); + const expectedOrigin = url.origin; + const secureFetch = async (input, init = {}) => { + const requestUrl = new URL(typeof input === 'string' || input instanceof URL ? input : input.url); + if (requestUrl.origin !== expectedOrigin) { + throw new Error('search-mcp URL policy: request origin changed after validation'); + } + return undiciFetch(input, { + ...init, + dispatcher: agent, + redirect: 'error', + ...(signal !== undefined ? { signal: combineSignals(signal, init.signal) } : {}), + }); + }; + + return { + transport: new StreamableHTTPClientTransport(url, { + fetch: secureFetch, + requestInit: { + headers, + redirect: 'error', + ...(signal !== undefined ? { signal } : {}), + }, + }), + close: () => agent.close(), + }; +} + +/** Build a stdio transport; the authParam name doubles as the env var name. */ +function stdioTransport(server, key) { + const env = { ...process.env }; + if (key !== undefined && key.length > 0 && server.authParam.length > 0) { + env[server.authParam] = `${server.authPrefix ?? ''}${key}`; + } + return new StdioClientTransport({ + command: server.command, + args: server.args ?? [], + env, + }); +} + +/** Race a protocol operation against the caller/timeout abort signal. */ +function race(promise, signal, stage) { + if (signal === undefined) return promise; + if (signal.aborted) throw aborted(stage); + return new Promise((resolve, reject) => { + const onAbort = () => { + signal.removeEventListener('abort', onAbort); + reject(aborted(stage)); + }; + signal.addEventListener('abort', onAbort, { once: true }); + promise.then( + (value) => { + signal.removeEventListener('abort', onAbort); + resolve(value); + }, + (error) => { + signal.removeEventListener('abort', onAbort); + reject(error); + }, + ); + }); +} + +function combineSignals(base, request) { + if (request === undefined || request === null || request === base) return base; + return AbortSignal.any([base, request]); +} + +function aborted(stage) { + return new WebError(`search-mcp: aborted while trying to ${stage}`, 'WEB_ABORTED'); +} diff --git a/lib/extract.js b/lib/extract.js new file mode 100644 index 0000000..87c0f2b --- /dev/null +++ b/lib/extract.js @@ -0,0 +1,117 @@ +/** + * Generic normalization of an MCP `tools/call` result into the + * `web_search` provider shape `{ sources, truncated, content? }`. + * + * Different search MCP servers return wildly different payloads (Tavily + * `results[]`, Brave `web.results[]`, Exa `results[]`, Perplexity text + + * citations, DuckDuckGo `results[]`...). Instead of mapping each vendor, we + * recursively walk the returned JSON and collect every object that carries a + * string `url` as a source, taking title / snippet / date from the common + * field names. A top-level `answer` (or non-JSON text blocks) becomes the + * `content` answer. + */ + +const TITLE_KEYS = ['title', 'name', 'headline']; +const SNIPPET_KEYS = ['snippet', 'content', 'description', 'text', 'excerpt', 'summary']; +const DATE_KEYS = [ + 'published_date', + 'publishedDate', + 'published_at', + 'publish_date', + 'publishedAt', + 'page_age', + 'age', + 'date', +]; + +/** Cap a snippet so a single source cannot blow up the context window. */ +const MAX_SNIPPET_CHARS = 600; +/** Cap the answer text block. */ +const MAX_CONTENT_CHARS = 4000; + +/** + * Project one MCP `tools/call` result into `{ sources, truncated, content? }`. + * + * @param result - the raw `CallToolResult` from the MCP SDK. + * @returns the normalized provider result; `truncated` is always false + * because the `ctx.web` seam owns the final `maxResults` cap. + */ +export function extractSearchResult(result) { + const bucket = { + sources: [], + seen: new Set(), + content: '', + }; + if (result !== null && typeof result === 'object') { + if (result.structuredContent !== undefined) collect(result.structuredContent, bucket); + const blocks = Array.isArray(result.content) ? result.content : []; + for (const block of blocks) { + if (block === null || typeof block !== 'object') continue; + if (block.type === 'json' && block.json !== undefined) { + collect(block.json, bucket); + } else if (block.type === 'text' && typeof block.text === 'string') { + const parsed = tryParseJson(block.text); + if (parsed !== undefined) collect(parsed, bucket); + else if (bucket.content.length === 0 && block.text.trim().length > 0) { + bucket.content = block.text.trim().slice(0, MAX_CONTENT_CHARS); + } + } + } + } + return { + sources: bucket.sources, + truncated: false, + ...(bucket.content.length > 0 ? { content: bucket.content } : {}), + }; +} + +/** Depth-first walk collecting source objects and the `answer` field. */ +function collect(node, bucket) { + if (Array.isArray(node)) { + for (const item of node) collect(item, bucket); + return; + } + if (node === null || typeof node !== 'object') return; + if (typeof node.url === 'string' && /^https?:\/\//i.test(node.url)) { + if (!bucket.seen.has(node.url)) { + bucket.seen.add(node.url); + const title = firstOf(node, TITLE_KEYS); + const snippet = truncate(firstOf(node, SNIPPET_KEYS), MAX_SNIPPET_CHARS); + const publishedAt = firstOf(node, DATE_KEYS); + bucket.sources.push({ + url: node.url, + ...(title !== undefined ? { title } : {}), + ...(snippet !== undefined ? { snippet } : {}), + ...(publishedAt !== undefined ? { publishedAt } : {}), + }); + } + return; + } + if (bucket.content.length === 0 && typeof node.answer === 'string' && node.answer.trim().length > 0) { + bucket.content = node.answer.trim().slice(0, MAX_CONTENT_CHARS); + } + for (const value of Object.values(node)) collect(value, bucket); +} + +/** First non-empty string among the candidate keys, else undefined. */ +function firstOf(node, keys) { + for (const key of keys) { + const value = node[key]; + if (typeof value === 'string' && value.trim().length > 0) return value.trim(); + } + return undefined; +} + +function truncate(value, max) { + if (value === undefined) return undefined; + return value.length > max ? `${value.slice(0, max)}…` : value; +} + +/** Parse a JSON text block; returns undefined when it is not JSON. */ +function tryParseJson(text) { + try { + return JSON.parse(text); + } catch { + return undefined; + } +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..725208b --- /dev/null +++ b/lib/index.js @@ -0,0 +1,112 @@ +/** + * dsh-search-mcp — replace dsh's built-in web search with search MCP servers. + * + * Adapted for DeepSeek Harness 0.1.2+: settings use + * `ctx.settings.installSection` (the old free-function + * `installSettingsSection` from 0.1.1-rc.2 no longer exists). + * + * A Cordis plugin that + * - registers a `ctx.web` search provider under the id `search-mcp`, and + * - installs a Settings section (`search-mcp`) where the user manages the + * search MCP server list (kind, endpoint/command, API key or key env + * reference, tool name) plus `defaultServer` / `maxResults` / + * `searchTimeoutMs` from the web Settings → Plugins page. + * + * The package's `cordis.patch.yml` (bundle layer) switches + * `web.searchProvider` to `search-mcp` and disables the built-in + * `web-search-deepseek` provider, so while this plugin is enabled the + * built-in search is unavailable and every `web_search` call runs through + * the configured MCP server(s). Removing the package restores the built-in. + */ +import z from '@deepseek-ai/schemastery'; +import { credentialRef } from '@deepseek-ai/dsh-credentials'; +import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'; +import { SearchMCPProvider } from './provider.js'; + +/** Cordis plugin name used by loader diagnostics. */ +export const name = 'search-mcp'; + +/** The web seam this provider registers into. */ +export const inject = ['web']; + +const serverSchema = z.object({ + id: z.string(), + kind: z.string().default('custom'), + transport: z.string().default('http'), + url: z.string().default(''), + command: z.string().default(''), + args: z.array(z.string()).default([]), + apiKey: z.string().role('secret'), + apiKeyEnv: z.string().role('credential-ref').default(''), + authStyle: z.string().default(''), + authParam: z.string().default(''), + authPrefix: z.string().default(''), + toolName: z.string().default(''), + // Note: this schemastery fork has no `.optional()`; object fields are + // optional unless `.required()` is applied, so absence is already allowed. + maxResults: z.number().step(1).min(1).max(50), +}); + +export const Config = z.object({ + defaultServer: z.string().default(''), + maxResults: z.number().step(1).min(1).max(50).default(8), + searchTimeoutMs: z.number().step(1).min(1000).default(30000), + servers: z.array(serverSchema).default([]), +}); + +/** Settings namespace owning this plugin's section (Settings → Plugins card). */ +export const SEARCH_MCP_SETTINGS_NAMESPACE = 'search-mcp'; + +/** Register the search provider and the live settings section. */ +export function apply(ctx, config) { + let current = () => config; + // Optional settings seam: fall back to the composition entry when settings + // is absent (same pattern as @deepseek-ai/dsh-web-search-deepseek). + ctx.inject(['settings'], (settingsCtx) => { + settingsCtx.settings.installSection(ctx, SEARCH_MCP_SETTINGS_NAMESPACE, Config, config, { + setSource: (source) => { + current = source; + }, + // Provider projects the section per search; no re-registration needed. + onChange: () => {}, + }); + }); + // `registerSearchProvider` owns its cleanup via ctx.effect (HMR/dispose safe). + ctx.web.registerSearchProvider(new SearchMCPProvider(() => resolveOptions(ctx, current()))); +} + +/** + * Project the authoritative config into per-search options. The section + * returned by `setSource` (settings.yaml `search-mcp:` block) replaces the + * row config entirely, matching how every other settings section behaves. + * + * @param ctx - plugin context supplying the credential and environment planes. + * @param config - the currently authoritative section. + * @returns options for one search. + */ +function resolveOptions(ctx, config) { + return { + servers: config.servers ?? [], + defaultServer: config.defaultServer ?? '', + maxResults: config.maxResults ?? 8, + searchTimeoutMs: config.searchTimeoutMs ?? 30000, + resolveKey: async (server) => { + if (server.apiKey !== undefined && server.apiKey.length > 0) return server.apiKey; + const envName = server.apiKeyEnv ?? ''; + if (envName.length === 0) return undefined; + const credentials = ctx.get('credentials'); + if (credentials !== undefined) { + try { + const resolved = await credentials.resolve(credentialRef(envName)); + if (resolved !== undefined && resolved.value !== undefined && resolved.value.length > 0) { + return resolved.value; + } + } catch { + /* fall through to the launch environment */ + } + } + const ambient = launchEnvironmentOf(ctx).get(envName); + return ambient !== undefined && ambient.value.length > 0 ? ambient.value : undefined; + }, + }; +} diff --git a/lib/provider.js b/lib/provider.js new file mode 100644 index 0000000..3dc22b4 --- /dev/null +++ b/lib/provider.js @@ -0,0 +1,83 @@ +/** + * The `search-mcp` web search provider. + * + * Registers into `ctx.web` under the stable id `search-mcp`; the profile + * patch switches `web.searchProvider` to this id and disables the built-in + * DeepSeek provider, so the model-facing `web_search` tool executes entirely + * through the configured search MCP server(s). + */ +import { WebError } from '@deepseek-ai/dsh-web'; +import { resolveServer } from './catalog.js'; +import { callMcpSearch } from './client.js'; +import { extractSearchResult } from './extract.js'; + +/** Stable provider id the `web` row's `searchProvider` config selects. */ +export const SEARCH_MCP_PROVIDER_ID = 'search-mcp'; + +/** The web search provider served by this plugin. */ +export class SearchMCPProvider { + id = SEARCH_MCP_PROVIDER_ID; + + /** + * @param resolveOptions - snapshots the authoritative config (row config, + * or the live settings section) at the START of each operation, so one + * search never mixes two settings saves. + */ + constructor(resolveOptions) { + this.resolveOptions = resolveOptions; + } + + /** Usable when at least one server entry exists; precise errors surface at search time. */ + available() { + const options = this.resolveOptions(); + return Array.isArray(options.servers) && options.servers.length > 0; + } + + async search(request, signal) { + const options = this.resolveOptions(); + const server = pickServer(options); + const resolved = resolveServer(server); + const maxResults = resolved.maxResults ?? options.maxResults ?? request.maxResults ?? 8; + const key = await options.resolveKey(resolved); + if (resolved.needsKey && (key === undefined || key.length === 0)) { + const ref = resolved.apiKeyEnv.length > 0 ? resolved.apiKeyEnv : 'apiKey'; + throw new WebError( + `search-mcp server "${resolved.id}" (${resolved.kind}) has no API key; set "apiKey" or a resolvable "apiKeyEnv" (${ref}) in Settings → Plugins → search-mcp`, + 'WEB_PROVIDER_ERROR', + ); + } + const combined = buildSignal(signal, options.searchTimeoutMs); + const outcome = await callMcpSearch(resolved, key, { query: request.query, maxResults }, combined); + return extractSearchResult(outcome); + } +} + +/** Select the default server, falling back to the first configured entry. */ +function pickServer(options) { + const servers = Array.isArray(options.servers) ? options.servers : []; + if (servers.length === 0) { + throw new WebError( + 'search-mcp: no search MCP servers configured; add one in Settings → Plugins → search-mcp', + 'WEB_PROVIDER_ERROR', + ); + } + if (options.defaultServer !== undefined && options.defaultServer.length > 0) { + const found = servers.find((entry) => entry.id === options.defaultServer); + if (found === undefined) { + throw new WebError( + `search-mcp: defaultServer "${options.defaultServer}" is not configured; known servers: ${servers + .map((entry) => `"${entry.id}"`) + .join(', ') || '(none)'}`, + 'WEB_PROVIDER_ERROR', + ); + } + return found; + } + return servers[0]; +} + +/** Combine the caller's cancellation with the configured timeout. */ +function buildSignal(signal, timeoutMs) { + const timeout = AbortSignal.timeout(timeoutMs); + return signal !== undefined ? AbortSignal.any([signal, timeout]) : timeout; +} diff --git a/lib/url-policy.js b/lib/url-policy.js new file mode 100644 index 0000000..c2a365d --- /dev/null +++ b/lib/url-policy.js @@ -0,0 +1,243 @@ +import { lookup as dnsLookup } from 'node:dns'; +import ipaddr from 'ipaddr.js'; + +const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']); + +/** + * Parse and resolve one HTTP endpoint before any request is sent. + * Every resolved address must be globally routable. + */ +export async function validateHttpEndpoint(input, options = {}) { + if (options.signal?.aborted) throw abortedPolicyError(); + if (typeof input !== 'string' || input.length === 0 || input !== input.trim()) { + throw policyError('endpoint must be a non-empty canonical URL'); + } + + let url; + try { + url = new URL(input); + } catch { + throw policyError('endpoint is not a valid URL'); + } + if (!ALLOWED_PROTOCOLS.has(url.protocol)) { + throw policyError('endpoint protocol must be http or https'); + } + if (url.username.length > 0 || url.password.length > 0) { + throw policyError('endpoint must not contain user information'); + } + if (url.hostname.length === 0) { + throw policyError('endpoint hostname is missing'); + } + + const hostname = stripIpv6Brackets(url.hostname).toLowerCase(); + const comparable = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname; + if (comparable === 'localhost' || comparable.endsWith('.localhost')) { + throw policyError('localhost endpoints are not allowed'); + } + rejectAmbiguousIpv4(input, comparable); + + let addresses; + if (ipaddr.isValid(comparable)) { + addresses = [{ address: normalizeAddress(comparable), family: addressFamily(comparable) }]; + } else { + addresses = await resolveAll(comparable, options.lookup ?? dnsLookup, options.signal); + } + if (addresses.length === 0) { + throw policyError('endpoint hostname did not resolve'); + } + + const normalized = deduplicateAddresses(addresses); + // Keep globally routable answers. Also allow RFC 2544 (198.18.0.0/15), which + // Clash/V2Ray fake-IP / TUN mode commonly returns for otherwise-public hosts. + // Real private/LAN answers are dropped; fail only when nothing usable remains. + const allowed = normalized.filter((record) => isAllowedEndpointAddress(record.address)); + if (allowed.length === 0) { + throw policyError('endpoint hostname resolves to a non-public address'); + } + + return Object.freeze({ + url, + hostname: comparable, + addresses: Object.freeze(allowed.map((record) => Object.freeze(record))), + lookup: createPinnedLookup(comparable, allowed), + }); +} + +/** Return true only for globally routable IPv4 or IPv6 addresses. */ +export function isPublicAddress(input) { + let address; + try { + address = ipaddr.parse(stripIpv6Brackets(input)); + } catch { + return false; + } + if (address.kind() === 'ipv6') { + if (address.isIPv4MappedAddress()) { + address = address.toIPv4Address(); + } else if (isIpv4CompatibleAddress(address)) { + return false; + } + } + return address.range() === 'unicast'; +} + +/** Addresses safe to dial for remote MCP endpoints (public or proxy fake-IP). */ +export function isAllowedEndpointAddress(input) { + if (isPublicAddress(input)) return true; + return isProxyFakeIpAddress(input); +} + +/** RFC 2544 benchmarking range used as DNS fake-IP by many local proxies. */ +export function isProxyFakeIpAddress(input) { + let address; + try { + address = ipaddr.parse(stripIpv6Brackets(input)); + } catch { + return false; + } + if (address.kind() === 'ipv6') { + if (address.isIPv4MappedAddress()) { + address = address.toIPv4Address(); + } else { + return false; + } + } + if (address.kind() !== 'ipv4') return false; + // 198.18.0.0/15 + const [a, b] = address.octets; + return a === 198 && (b === 18 || b === 19); +} + +/** Build a Node-compatible DNS lookup that never resolves beyond the pinned set. */ +export function createPinnedLookup(hostname, records) { + const target = normalizeHostname(hostname); + const frozen = deduplicateAddresses(records); + let cursor = 0; + return (requested, options, callback) => { + const requestedHost = normalizeHostname(requested); + if (requestedHost !== target) { + const error = policyError('connection attempted an unvalidated hostname'); + error.code = 'EACCES'; + queueMicrotask(() => callback(error)); + return; + } + + const lookupOptions = typeof options === 'object' && options !== null ? options : {}; + const family = Number(lookupOptions.family) || 0; + const candidates = family === 4 || family === 6 + ? frozen.filter((record) => record.family === family) + : frozen; + if (candidates.length === 0) { + const error = policyError('no validated address matches the requested family'); + error.code = 'ENOTFOUND'; + queueMicrotask(() => callback(error)); + return; + } + if (lookupOptions.all === true) { + queueMicrotask(() => callback(null, candidates.map((record) => ({ ...record })))); + return; + } + const selected = candidates[cursor++ % candidates.length]; + queueMicrotask(() => callback(null, selected.address, selected.family)); + }; +} + +function resolveAll(hostname, lookup, signal) { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(abortedPolicyError()); + return; + } + let settled = false; + const finish = (callback, value) => { + if (settled) return; + settled = true; + signal?.removeEventListener('abort', onAbort); + callback(value); + }; + const onAbort = () => finish(reject, abortedPolicyError()); + signal?.addEventListener('abort', onAbort, { once: true }); + lookup(hostname, { all: true, verbatim: true }, (error, records) => { + if (settled) return; + if (error) { + finish(reject, policyError('endpoint hostname resolution failed')); + return; + } + const list = Array.isArray(records) ? records : records === undefined ? [] : [records]; + try { + finish(resolve, list.map((record) => { + const raw = typeof record === 'string' ? record : record.address; + return { address: normalizeAddress(raw), family: addressFamily(raw) }; + })); + } catch { + finish(reject, policyError('endpoint hostname returned an invalid address')); + } + }); + }); +} + +function deduplicateAddresses(records) { + const seen = new Set(); + const result = []; + for (const record of records) { + const address = normalizeAddress(record.address); + const family = addressFamily(address); + const key = `${family}:${address}`; + if (seen.has(key)) continue; + seen.add(key); + result.push({ address, family }); + } + return result; +} + +function normalizeAddress(input) { + let address = ipaddr.parse(stripIpv6Brackets(input)); + if (address.kind() === 'ipv6' && address.isIPv4MappedAddress()) { + address = address.toIPv4Address(); + } + return address.toNormalizedString(); +} + +function addressFamily(input) { + const address = ipaddr.parse(stripIpv6Brackets(input)); + if (address.kind() === 'ipv6' && address.isIPv4MappedAddress()) return 4; + return address.kind() === 'ipv4' ? 4 : 6; +} + +function rejectAmbiguousIpv4(input, hostname) { + const authority = input.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]+)/)?.[1] ?? ''; + const rawHost = authority.startsWith('[') + ? authority.slice(1, authority.indexOf(']')) + : authority.replace(/:\d*$/, ''); + if (!rawHost.includes(':') && ipaddr.IPv4.isValid(rawHost)) { + const canonical = ipaddr.IPv4.parse(rawHost).toString(); + if (rawHost !== canonical || hostname !== canonical) { + throw policyError('endpoint contains a non-canonical IPv4 address'); + } + } +} + +function isIpv4CompatibleAddress(address) { + return address.parts.slice(0, 6).every((part) => part === 0); +} + +function normalizeHostname(input) { + const value = stripIpv6Brackets(String(input)).toLowerCase(); + return value.endsWith('.') ? value.slice(0, -1) : value; +} + +function stripIpv6Brackets(input) { + return input.startsWith('[') && input.endsWith(']') ? input.slice(1, -1) : input; +} + +function abortedPolicyError() { + const error = policyError('endpoint validation was aborted'); + error.code = 'ABORT_ERR'; + return error; +} + +function policyError(message) { + const error = new Error(`search-mcp URL policy: ${message}`); + error.name = 'SearchMcpUrlPolicyError'; + return error; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..62d662f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2202 @@ +{ + "name": "dsh-search-mcp", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsh-search-mcp", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-api-remotes": "0.1.1-rc.2", + "@deepseek-ai/dsh-credentials": "0.1.1-rc.2", + "@deepseek-ai/dsh-launch-environment": "0.1.1-rc.2", + "@deepseek-ai/dsh-settings": "0.1.1-rc.2", + "@deepseek-ai/dsh-web": "0.1.1-rc.2", + "@deepseek-ai/schemastery": "3.18.1", + "@modelcontextprotocol/sdk": "1.30.0", + "ipaddr.js": "2.5.0", + "undici": "6.28.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@deepseek-ai/cordis": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis/-/cordis-4.0.1.tgz", + "integrity": "sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "@standard-schema/spec": "^1.1.0" + }, + "bin": { + "cordis": "bin.js" + }, + "peerDependencies": { + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2" + }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis-plugin-include": { + "optional": true + }, + "@deepseek-ai/cordis-plugin-loader": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/cordis-plugin-include": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-include/-/cordis-plugin-include-1.0.6.tgz", + "integrity": "sha512-i1VXrZCbv6tk/iUgedCNjrxxArbWT3IvRZGB5sdqJ3ectnihivXXQbRZ8JJ73DSmAPvlMGmrbtjFAfm10yvXRg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "js-yaml": "^4.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2" + } + }, + "node_modules/@deepseek-ai/cordis-plugin-loader": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-loader/-/cordis-plugin-loader-1.0.2.tgz", + "integrity": "sha512-RIW9hoVyhYDWdCI9BsvtZccPde1ECLC4OAxupwowGTak78vwVTVdb3HezTSOK1Y1/Ax3Ru0LA1pYOB04CnTxIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "node-addon-require-builtin": "^0.1.4" + }, + "peerDependenciesMeta": { + "node-addon-require-builtin": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/cosmokit": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/cosmokit/-/cosmokit-1.8.2.tgz", + "integrity": "sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==", + "license": "MIT" + }, + "node_modules/@deepseek-ai/dsh-agent": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent/-/dsh-agent-0.1.1-rc.2.tgz", + "integrity": "sha512-cC7lnJe7JgPFcreNXxcxLMxQd78LnpVO9ZXROjZsGRQN1zGH6i/DduI892F1am85IfzzO+XTxMwwUHmfwamb0g==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-agent-default-model": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-default-model/-/dsh-agent-default-model-0.1.1-rc.2.tgz", + "integrity": "sha512-Pv+4p20Eol7Ds/n0OS0vjtChCWfFTJAMThxugXcEcLKEJ9WAtpI4mzWfLgjmeVXnwD2yvp6HlLKDrrQV9PIkww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-settings": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-agent-presets": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-agent-presets/-/dsh-agent-presets-0.1.1-rc.2.tgz", + "integrity": "sha512-88r3jkrbdwTgcP3MZLIUX458Ecu8JcLmZa7PtPszwf33yFoWlZvZmgjyn5ETHV55plNQ8gKMRm9a0RwzGGmzCw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "js-yaml": "^4.1.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-atomic-write": "^0.1.1-rc.2", + "@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-settings": "^0.1.1-rc.2", + "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-api-gateway": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-api-gateway/-/dsh-api-gateway-0.1.1-rc.2.tgz", + "integrity": "sha512-i/e/Ecg1QCjMePUFHBfjRQU3NbDV0IdORwQbQD6RpiirzyvAIm4vvZgmW/FfgkO2XYJHHIVjo3i1i0YaHPcQag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-registry": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-api-remotes": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-api-remotes/-/dsh-api-remotes-0.1.1-rc.2.tgz", + "integrity": "sha512-mtmMxA0TZwTjy46E2DF0kmsPCXVq6RBhrwEHWXBaRzc0DLjnYqmJODrZ0X+XwXOkwFZtDxWSv/uheiIsXf8now==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2", + "@deepseek-ai/dsh-api-gateway": "^0.1.1-rc.2", + "@deepseek-ai/dsh-commands": "^0.1.1-rc.2", + "@deepseek-ai/dsh-cordis-host-runner": "^0.1.1-rc.2", + "@deepseek-ai/dsh-credentials": "^0.1.1-rc.2", + "@deepseek-ai/dsh-file-reference": "^0.1.1-rc.2", + "@deepseek-ai/dsh-goal": "^0.1.1-rc.2", + "@deepseek-ai/dsh-host-plugin-inventory": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-message-feedback": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-reference": "^0.1.1-rc.2", + "@deepseek-ai/dsh-settings": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-registry": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-atomic-write": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-atomic-write/-/dsh-atomic-write-0.1.1-rc.2.tgz", + "integrity": "sha512-QqNSF0+Ddn6qWY480dlilwEy6FLv3JKEWx1UQgoNJrxD4y54SDRzqBQB9yDXWKOoOGyC+05TN6/Px10GNIzMWA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-attachment": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-attachment/-/dsh-attachment-0.1.1-rc.2.tgz", + "integrity": "sha512-rCYAt8QsawP1yfDCU7XxNwYT/XWvyFsxYrkwhLLkdfW83QVD0CQHizSkTQE7RFX74nKUD1z3sTLfnLr7xneArw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-brand": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-brand/-/dsh-brand-0.1.1-rc.2.tgz", + "integrity": "sha512-8vXsAXoUdzKAgvd/E9DgyT6HKmR6ZM4rtJ3fs/XoJ6n2kBk4tWil8Lv+jxCMWJeMOnTJF55gu2+NWQ3ECFPtJw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-client-connection": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-client-connection/-/dsh-client-connection-0.1.1-rc.2.tgz", + "integrity": "sha512-YX2WLA/aZdDQsien4Zo7IHTEfYVJ+4QhRXbgA6BrRUM23NSP/+V3K00dQYyQVsF3ZwocE5uyvlZvbYeg1Iz4ug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "ws": "^8.21.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2", + "@deepseek-ai/dsh-commands": "^0.1.1-rc.2", + "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.2", + "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-tools": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-code-runtime": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-code-runtime/-/dsh-code-runtime-0.1.1-rc.2.tgz", + "integrity": "sha512-SgFresqH5UABzRQZ7tOfqzOLMHF7089VeH+mfcwNQH5peOavgEKrAGOYz/9RnISH0XmMrj/x177t8gfO8Uvo/w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-commands": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-commands/-/dsh-commands-0.1.1-rc.2.tgz", + "integrity": "sha512-BOIe4Sht9rmMv1a6b3GWjWBbeWr7PtHlAy41vgpaymvUUuzOapOIA648ZMGCI/crRIt72Umev2FHtSwCNSbYZg==", + "license": "MIT", + "peer": true, + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-compaction": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-compaction/-/dsh-compaction-0.1.1-rc.2.tgz", + "integrity": "sha512-LV5GAIx7GO8DCRivnN2bmLmuucsYDG+ifG18BaXBqsVKdrzmaIu5o+CBxQAI2bX1N6mfBenLxmvCnROkyumLTg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-commands": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-cordis-host-runner": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-cordis-host-runner/-/dsh-cordis-host-runner-0.1.1-rc.2.tgz", + "integrity": "sha512-+AQGegPy+gdtcjTTPxDDYwAFUn6BB1J/MFFbmpPr+WIqJoDyVsuCDe8Rr8mCJmxs12CmTZXXSOE03LW203W8fA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-tools": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-credentials": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-credentials/-/dsh-credentials-0.1.1-rc.2.tgz", + "integrity": "sha512-aeVBaH07rox7NuSNbSqbz8g0eNb2IIhNbrZngj/VxUsr/TR9TXOq7lm1CL6SBUDlstGw3vNWeXyhim/DmA5iSQ==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-file-reference": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-file-reference/-/dsh-file-reference-0.1.1-rc.2.tgz", + "integrity": "sha512-8Pd4SNHhV6OlbwfV5K4qmFb709I0Z68qgaUQJ7zM4BTjClNe1/dktQnETtWPfqbPTx/2sNKXM1rz4WXuROrIfQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-goal": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-goal/-/dsh-goal-0.1.1-rc.2.tgz", + "integrity": "sha512-lSHTh4vfS6eRb9to/y+bjRf2+0QkNpY3tHJ29HMTewR9fJYZsEVVu4Hc+GPhPEjF7RpiD35/sKx+akijtDasyg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-home-paths": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-home-paths/-/dsh-home-paths-0.1.1-rc.2.tgz", + "integrity": "sha512-lGsP7sbnu20AiRAb+gxYiKx9oa9r1D/8Fr6BwWHXpaPx6ZE4Qg0+ybh5SZVfGQ+XhLcQisu3nwWemEjtTJGiig==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-host-apiproxy": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-apiproxy/-/dsh-host-apiproxy-0.1.1-rc.2.tgz", + "integrity": "sha512-dplRnGGXXsQYFQ1KMHymAM0iaxuE9Z153JHYcGEgOwXNkS3HA20gSi3yMt6fz+zi/cMHYXvY1JQhS54BTc761A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-agent-default-model": "^0.1.1-rc.2", + "@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2", + "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-commands": "^0.1.1-rc.2", + "@deepseek-ai/dsh-credentials": "^0.1.1-rc.2", + "@deepseek-ai/dsh-goal": "^0.1.1-rc.2", + "@deepseek-ai/dsh-host-directory-picker": "^0.1.1-rc.2", + "@deepseek-ai/dsh-jobs": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-native-command": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection-cache": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-query": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-title": "^0.1.1-rc.2", + "@deepseek-ai/dsh-settings": "^0.1.1-rc.2", + "@deepseek-ai/dsh-skill": "^0.1.1-rc.2", + "@deepseek-ai/dsh-subagent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-tools": "^0.1.1-rc.2", + "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2", + "@deepseek-ai/dsh-user-questions": "^0.1.1-rc.2", + "@deepseek-ai/dsh-workspace": "^0.1.1-rc.2", + "@deepseek-ai/schemastery": "^3.18.1", + "fflate": "^0.8.2", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2", + "@deepseek-ai/dsh-cordis-host-runner": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-host-directory-picker": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-directory-picker/-/dsh-host-directory-picker-0.1.1-rc.2.tgz", + "integrity": "sha512-m3puwS+bvJQ1eASdpEEwlQqKa9znhBSgtoSSI0GZN5VMynZVl7E31HMltENz3QC2Nd+C3bh7vNjrJFtumUDsLQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-host-plugin-inventory": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-plugin-inventory/-/dsh-host-plugin-inventory-0.1.1-rc.2.tgz", + "integrity": "sha512-Hud9ezW0bexWfhX7C+c5rdUDX1xzbEGDzj1lGQyj/QxdrxHYHjGrJq3tLRyvN6K4FSmEdG2IBKdQGCOLVrIthA==", + "license": "MIT", + "peer": true, + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-host-webserver": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-host-webserver/-/dsh-host-webserver-0.1.1-rc.2.tgz", + "integrity": "sha512-t9MrjC65QHiiWhG9V8UZxgfE/aWYhJHHrIM0kbTvtXxg4tLGIKo/upHp7iiag65F3HTkVLrH/DUyPMi4v2ZA7g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-invariants": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-invariants/-/dsh-invariants-0.1.1-rc.2.tgz", + "integrity": "sha512-l+1Om/EDFyMjhgSuEx2WDLLA2fia/+ga9mBTCoT/MMslsnWaK5G0/lWwbwlTBSaJ6OfmYc3DuBgox8DbgIGHRQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1" + } + }, + "node_modules/@deepseek-ai/dsh-jobs": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-jobs/-/dsh-jobs-0.1.1-rc.2.tgz", + "integrity": "sha512-SXvDJMvcUrGrlzIyE7j8/lI4Pj1nDe/UOR8C05Zagp+/0R8p46n6KylySvZdPAFENV5t8WX3Fw3eOaS4No0+wQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-launch-environment": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-launch-environment/-/dsh-launch-environment-0.1.1-rc.2.tgz", + "integrity": "sha512-LIjPUPwaZ2cIiz98Oqxn9TDKXhWlp+0EGmUhk4RerBAkqRVtDl0B/leK1pSZLLIR+qcr+VAwLGJagY8Xm9XWvw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-llm": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-llm/-/dsh-llm-0.1.1-rc.2.tgz", + "integrity": "sha512-ASJfjIdZbIXvLwi3rGo+eZb/GxMVV/WO5/XVD3B96mT8EIzrlw3+nMR6/CvmJVzcycKQ2XN0wj7jD6TasPRySA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-timeout": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-message-feedback": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-message-feedback/-/dsh-message-feedback-0.1.1-rc.2.tgz", + "integrity": "sha512-GzxDiNvyUVs/bbbm+Hr3MfuU3wM8ErRF5z9XadnSGPYZvipZDAMrvrS5v6JgOUaZt6ApBpEMJ4xVgIel1VnGYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-native-command": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-native-command/-/dsh-native-command-0.1.1-rc.2.tgz", + "integrity": "sha512-GIknPrwU7vZOUQ2o/ES7Z0uUhUrZGp/yigKP+RXEu41VI9tcybiXQAA8CEdDFmNQ11R2wfYQhr3WO5vy35akWg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-output-retention": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-output-retention/-/dsh-output-retention-0.1.1-rc.2.tgz", + "integrity": "sha512-tCni+bTEp/FWokfz3fqn4p6SzHn6pkY6H3HkS9UY5PHrztUE1ESUQUp41kIVtwUhRmJU7yissqjxe6kLDB6t2Q==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-scope": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-scope/-/dsh-scope-0.1.1-rc.2.tgz", + "integrity": "sha512-Xy3ejL6dwVSluZL7XOWy76ya4pCw1uHwxodDK4O9XiQUiUV4FBXnt0aNJUtMeAFN0c1YujxxCmRniMvuuNn1Nw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-session": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session/-/dsh-session-0.1.1-rc.2.tgz", + "integrity": "sha512-4/cv6X9HPhm47eyRhCu/WZwzrtJKegk5J+0xaxcZ9i8S0smdxP57tqy8a0jkSshLQn7BzMFxneQrlYExrLrDhQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-session-persistence": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-persistence/-/dsh-session-persistence-0.1.1-rc.2.tgz", + "integrity": "sha512-dxdYxRfmK5jWtiFFabqRNb/jGGjkXyF2djI7O8IIKmDVjhQiv170zpvhbAhRUuqClEdseCtbQpLBrRm2blzt3g==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-timeout": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-session-projection": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-projection/-/dsh-session-projection-0.1.1-rc.2.tgz", + "integrity": "sha512-SNaOrjRS4RMXocna6uL33TeS/uhcQ7jDJtJZ1HyDPL06mXUdAgje2MVt8OZCh6dH95xIiqpQQm+KjzeiQnhYSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-session-projection-cache": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-projection-cache/-/dsh-session-projection-cache-0.1.1-rc.2.tgz", + "integrity": "sha512-UvCRpIb+LoQI/nCLof17xc2P2KuZoucU3VuzfAukfsF3dYZAy5OP7jrCRdVZIvjRfvFUd7G+awdS9sWbnjcolg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2", + "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-session-query": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-query/-/dsh-session-query-0.1.1-rc.2.tgz", + "integrity": "sha512-QxQFRg/KnrZnYiO7fNtTJVTakuGs9ZFRkGGgYuNPzSz1pRqiohGNE/JZzBtq+HMv38RRTRZrM4aIOUV8OgqoXQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-title": "^0.1.1-rc.2" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-session-persistence": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-session-reference": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-reference/-/dsh-session-reference-0.1.1-rc.2.tgz", + "integrity": "sha512-c9mz19ndxjVxSnXEhmJwGDjyKG4oNmu4Sfneix8OUJ+mAr3jIgb/QZZ8rYEc8Bi+Dd1z7Wr90IeWOBF0nolU3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-compaction": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-output-retention": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-query": "^0.1.1-rc.2", + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-session-title": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-session-title/-/dsh-session-title-0.1.1-rc.2.tgz", + "integrity": "sha512-qHv+9nE6J/piHsWwckmbJBS1sJjunCWsv00arhsgjW1XMLCHxG6QOB+U48P4EBZjve798Tz8AQIlWZy+9T8XmA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-settings": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-settings/-/dsh-settings-0.1.1-rc.2.tgz", + "integrity": "sha512-iGdKEt91Im3gE7xA9CzRfTJsPcFcxDeDOCLhAjzbpjEv7TAjx/EoYP7lPtiy+QmOjKSKy206SEtAKol9PXWbOw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/schemastery": "^3.18.1" + } + }, + "node_modules/@deepseek-ai/dsh-skill": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-skill/-/dsh-skill-0.1.1-rc.2.tgz", + "integrity": "sha512-FACjlOqdsWX+0RtSs3RWrdY0QQEpPJrBfvxUbWSh7E8UyCM8dKdIbsQnuXIjsAgC7GgYp7lyFDSCMovQ3ARp8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-storage": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-storage/-/dsh-storage-0.1.1-rc.2.tgz", + "integrity": "sha512-ptJ1ss8spF+Y2VXjsMV37Qh1+hviYWZylDvXtfEBXjLbPi/BP3dktT4B6OMIAD1rRN+0A4HdTqJBgLk7Gp1rig==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-storage-domain": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-storage-domain/-/dsh-storage-domain-0.1.1-rc.2.tgz", + "integrity": "sha512-9/3OuaZpxf9NZWhrARpgZ7UBa03pPaTIiDBw+SfESfwVk42NNLVCQgvnEbrWoNwdtzmv0aYCEV23sKwwlQ0LBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-storage": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-subagent": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-subagent/-/dsh-subagent-0.1.1-rc.2.tgz", + "integrity": "sha512-CNa0WuFCR69TMnBKYQInz49/olDSaVHiYkDTyTuDh7YW7Y80/f/HjQe5G0fQaqZMLla3IUSXCIPl5EssWmjcQw==", + "license": "MIT", + "peer": true, + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-jobs": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2", + "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-projection-cache": "^0.1.1-rc.2", + "@deepseek-ai/dsh-tools": "^0.1.1-rc.2", + "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-agent-presets": { + "optional": true + }, + "@deepseek-ai/dsh-jobs": { + "optional": true + }, + "@deepseek-ai/dsh-sandbox": { + "optional": true + }, + "@deepseek-ai/dsh-sandbox-policy": { + "optional": true + }, + "@deepseek-ai/dsh-session-persistence": { + "optional": true + }, + "@deepseek-ai/dsh-session-projection": { + "optional": true + }, + "@deepseek-ai/dsh-session-projection-cache": { + "optional": true + }, + "@deepseek-ai/dsh-user-approval": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/dsh-system-prompt": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-system-prompt/-/dsh-system-prompt-0.1.1-rc.2.tgz", + "integrity": "sha512-on4hjAlYI5uX9q7Sf95YkMMBVe6heywtA/H50ksrIMUub8U2B98hO9iQpHhjwIO1F1vu+5pLcPvRr6yUGGmtXQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-timeout": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.1.1-rc.2.tgz", + "integrity": "sha512-RrouVgU3G5gXr9zHhpThkMG6YKdcRJzXXdPm1dq3ioBxbvxlfMSfNY4tN8lWMJxLyGtvWkPra0HQX+YWxvdOOA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-tools": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-tools/-/dsh-tools-0.1.1-rc.2.tgz", + "integrity": "sha512-0GGL4D55MwYDepzZMOI3L0ycu5b2qr96GL0Y7snwhAnpK2Di61rbX3fJE+PB3ZrovGX0csIRdt9n3iJZDVtDrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-code-runtime": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2", + "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-typert-protocol": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-protocol/-/dsh-typert-protocol-0.1.1-rc.2.tgz", + "integrity": "sha512-lxBssDc5Pz1qBE5kuIyaArA7AvIPq9rpaVclylodiSzVJe95e2xruBg73tflyjtd8y00toet+DLgQ6tSSsq6Kw==", + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-typert-registry": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-registry/-/dsh-typert-registry-0.1.1-rc.2.tgz", + "integrity": "sha512-ATZu3i7UId+ktsWW3pUFm5Hi1fVsRXFmByATVPp2RlxH1zjxXnauVW6k70ZwU/4FOsphRzo65SXOlAXA+natYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-user-approval": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-user-approval/-/dsh-user-approval-0.1.1-rc.2.tgz", + "integrity": "sha512-SdsO4Rs+NeJFoertkVilXBACREOLfkKPJJznYKqDhJxeRo38RJ56dtj0Xd0/6rERmsQiMck4Bwdrzg1ubUqPNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", + "@deepseek-ai/dsh-scope": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-user-questions": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-user-questions/-/dsh-user-questions-0.1.1-rc.2.tgz", + "integrity": "sha512-9lYoB7qCFE+Vvgwjny3MnRfS7QUefOspj4KpE3b30DAAJfxwrU4hRtLCHOKxyurF01qrkoEBBpXn4H5ilXYTKg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-web": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-web/-/dsh-web-0.1.1-rc.2.tgz", + "integrity": "sha512-Rtikc8RUlfx6m9+hYvKB3JlAD34PrI2UFHEu1h8UMb7pM/ulIRpjBF7TPlg537WRK4ufCysqCJm0RheNMoYCcQ==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-workspace": { + "version": "0.1.1-rc.2", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/dsh-workspace/-/dsh-workspace-0.1.1-rc.2.tgz", + "integrity": "sha512-jBUob4H5TZAiExq9YNVCglKAFmAKMtd1UbyqFfnZZ1Owm+3c3NbAXY947MHiD6NwCwFEW1y7FjrFj66UQvG90A==", + "license": "MIT", + "peer": true, + "dependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2", + "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2", + "@deepseek-ai/dsh-storage": "^0.1.1-rc.2", + "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2" + } + }, + "node_modules/@deepseek-ai/schemastery": { + "version": "3.18.1", + "resolved": "https://registry.npmmirror.com/@deepseek-ai/schemastery/-/schemastery-3.18.1.tgz", + "integrity": "sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "@standard-schema/spec": "^1.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-2.1.0.tgz", + "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/@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/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0", + "peer": true + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/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.0.0", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT", + "peer": true + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmmirror.com/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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..ae984fc --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "dsh-search-mcp", + "version": "0.2.0", + "description": "Replace dsh's built-in web search with search MCP servers (Tavily / Brave / Exa / Perplexity / DuckDuckGo / custom), configured from the web Settings page. When this plugin is enabled the built-in DeepSeek search provider is disabled.", + "type": "module", + "main": "lib/index.js", + "exports": { + ".": "./lib/index.js", + "./client": "./lib/client.browser.js", + "./cordis.patch.yml": "./cordis.patch.yml", + "./package.json": "./package.json" + }, + "files": [ + "lib", + "cordis.patch.yml" + ], + "license": "MIT", + "engines": { + "node": ">=20" + }, + "scripts": { + "check": "node --check lib/index.js && node --check lib/provider.js && node --check lib/catalog.js && node --check lib/extract.js && node --check lib/url-policy.js && node --check lib/client.js && node --check lib/client.browser.js", + "test": "node --test" + }, + "keywords": [ + "dsh", + "dsh-plugin", + "mcp", + "search", + "web_search", + "tavily", + "brave", + "exa", + "perplexity", + "duckduckgo" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + }, + "client": { + "platform": "web", + "immediately": true, + "inject": [ + "@deepseek-ai/dsh-client-ui-settings-plugins", + "@deepseek-ai/dsh-client-locale" + ] + } + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.30.0", + "ipaddr.js": "2.5.0", + "undici": "6.28.0" + }, + "peerDependencies": { + "@deepseek-ai/dsh-api-remotes": "*", + "@deepseek-ai/dsh-credentials": "*", + "@deepseek-ai/dsh-launch-environment": "*", + "@deepseek-ai/dsh-settings": "*", + "@deepseek-ai/dsh-web": "*", + "@deepseek-ai/schemastery": "*" + } +} diff --git a/test/compatibility.test.js b/test/compatibility.test.js new file mode 100644 index 0000000..ff4183b --- /dev/null +++ b/test/compatibility.test.js @@ -0,0 +1,79 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const read = (path) => readFile(resolve(root, path), 'utf8'); + +test('package exports resolve and RC2 dependencies stay pinned', async () => { + const pkg = JSON.parse(await read('package.json')); + assert.equal(pkg.exports['.'], './lib/index.js'); + assert.equal(pkg.exports['./client'], './lib/client.browser.js'); + assert.equal(pkg.engines.node, '>=20'); + + for (const name of [ + '@deepseek-ai/dsh-api-remotes', + '@deepseek-ai/dsh-credentials', + '@deepseek-ai/dsh-launch-environment', + '@deepseek-ai/dsh-settings', + '@deepseek-ai/dsh-web', + ]) { + assert.equal(pkg.dependencies[name], '0.1.1-rc.2'); + } + assert.equal(pkg.dependencies['@deepseek-ai/schemastery'], '3.18.1'); + assert.equal(pkg.dependencies.undici, '6.28.0'); + assert.equal(pkg.dependencies['ipaddr.js'], '2.5.0'); + assert.equal(pkg.dependencies['@modelcontextprotocol/sdk'], '1.30.0'); +}); + +test('RC2 browser bundle uses keyed settings slot and credential migration', async () => { + const client = await read('lib/client.browser.js'); + assert.match(client, /name: "settings\.plugin\.item",\s+key: NS,/); + assert.doesNotMatch(client, /name: "settings\.plugin\.item",\s+id:/); + assert.match(client, /api\.settings\.describe\(\{\}\)/); + assert.match(client, /api\.credentials\.describe\(\{ refs: refs\.slice\(index, index \+ CREDENTIAL_DESCRIBE_BATCH_SIZE\) \}\)/); + assert.match(client, /api\.credentials\.set\(\{ ref, value \}\)/); + assert.match(client, /credentials\/reference-updated/); + assert.match(client, /CREDENTIAL_DESCRIBE_BATCH_SIZE = 64/); + assert.match(client, /api\.credentials\.unset\(\{ ref \}\)/); + assert.match(client, /rollbackSettingsWrites/); + assert.match(client, /legacyKeyBlocked/); + assert.match(client, /deepEqualJson\(current\[field\], value\)/); +}); + +test('known providers are CDKey-only while custom keeps advanced fields', async () => { + const client = await read('lib/client.browser.js'); + const catalog = client.slice(client.indexOf('const CATALOG = {'), client.indexOf('const KIND_OPTIONS')); + assert.doesNotMatch(catalog, /https?:\/\//); + assert.doesNotMatch(catalog, /toolName|authParam|transport/); + assert.match(client, /const known = row\.kind !== "custom"/); + assert.match(client, /children: known \? \[/); + assert.match(client, /No endpoint is required for known providers/); + assert.match(client, /已知提供商不需要填写端点链接/); + assert.match(client, /kind, apiKey: "", apiKeyEnv: ""/); +}); + +test('HTTP transport pins DNS and applies one guarded fetch to every SDK request', async () => { + const transport = await read('lib/client.js'); + assert.match(transport, /validateHttpEndpoint\(server\.url, \{ signal \}\)/); + assert.match(transport, /new Agent\(\{[\s\S]*connect: \{ lookup: validated\.lookup \}/); + assert.match(transport, /requestUrl\.origin !== expectedOrigin/); + assert.match(transport, /dispatcher: agent/); + assert.match(transport, /redirect: 'error'/); + assert.match(transport, /fetch: secureFetch/); + assert.match(transport, /await client\.close\(\)[\s\S]*await runtime\?\.close\(\)/); + assert.doesNotMatch(transport, /new URL\(server\.url\)[\s\S]*new StreamableHTTPClientTransport\(url, \{\s*requestInit:/); +}); +test('bundle replaces built-in search and leaves default row endpoint-free', async () => { + const patch = await read('cordis.patch.yml'); + assert.match(patch, /searchProvider: search-mcp/); + assert.match(patch, /- id: web-search-deepseek\s+disabled: true/); + assert.match(patch, /- id: tool-web\s+disabled: false/); + assert.match(patch, /fetch: false/); + assert.match(patch, /searchMaxResults: 50/); + assert.match(patch, /searchMaxQueries: 4/); + const defaultRow = patch.slice(patch.indexOf('- id: tavily'), patch.indexOf('- id: web')); + assert.doesNotMatch(defaultRow, /url:|toolName:|authParam:|transport:/); +}); diff --git a/test/runtime.test.js b/test/runtime.test.js new file mode 100644 index 0000000..93c8e8a --- /dev/null +++ b/test/runtime.test.js @@ -0,0 +1,130 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { clampSearchResults, SEARCH_MCP_CATALOG, resolveServer } from '../lib/catalog.js'; +import { extractSearchResult } from '../lib/extract.js'; + +test('catalog exposes every supported provider preset', () => { + assert.deepEqual(Object.keys(SEARCH_MCP_CATALOG), [ + 'tavily', + 'brave', + 'exa', + 'perplexity', + 'duckduckgo', + 'custom', + ]); +}); + +test('known providers ignore stored connection overrides', () => { + const tavily = resolveServer({ + id: 'primary', + kind: 'tavily', + transport: 'stdio', + url: 'https://example.test/mcp', + authStyle: 'header', + authParam: 'X-Other-Key', + toolName: 'other_search', + apiKeyEnv: 'MY_TAVILY_KEY', + maxResults: 12, + }); + assert.equal(tavily.transport, 'http'); + assert.equal(tavily.url, 'https://mcp.tavily.com/mcp/'); + assert.equal(tavily.authStyle, 'query'); + assert.equal(tavily.authParam, 'tavilyApiKey'); + assert.equal(tavily.toolName, 'tavily_search'); + assert.equal(tavily.countArg, 'max_results'); + assert.equal(tavily.apiKeyEnv, 'MY_TAVILY_KEY'); + assert.equal(tavily.maxResults, 12); +}); + +test('prototype property kinds fall back to custom', () => { + for (const kind of ['constructor', 'toString', '__proto__']) { + const resolved = resolveServer({ id: kind, kind, url: 'https://search.example/mcp' }); + assert.equal(resolved.kind, 'custom'); + assert.equal(resolved.url, 'https://search.example/mcp'); + } +}); + +test('custom providers preserve advanced connection fields', () => { + const custom = resolveServer({ + id: 'custom', + kind: 'custom', + transport: 'stdio', + command: 'custom-mcp', + args: ['--stdio'], + authStyle: 'header', + authParam: 'X-Key', + authPrefix: 'Token ', + toolName: 'search', + }); + assert.equal(custom.transport, 'stdio'); + assert.equal(custom.command, 'custom-mcp'); + assert.deepEqual(custom.args, ['--stdio']); + assert.equal(custom.authParam, 'X-Key'); + assert.equal(custom.authPrefix, 'Token '); + assert.equal(custom.toolName, 'search'); +}); + +test('provider contracts match current upstream transports', () => { + const brave = resolveServer({ id: 'brave', kind: 'brave' }); + assert.equal(brave.transport, 'stdio'); + assert.equal(brave.command, 'npx'); + assert.deepEqual(brave.args, ['-y', '@brave/brave-search-mcp-server@2.1.3']); + assert.equal(brave.authParam, 'BRAVE_API_KEY'); + + const perplexity = resolveServer({ id: 'perplexity', kind: 'perplexity' }); + assert.equal(perplexity.url, 'https://api.perplexity.ai/mcp'); + assert.equal(perplexity.authParam, 'Authorization'); + assert.equal(perplexity.authPrefix, 'Bearer '); + assert.equal(perplexity.toolName, 'perplexity_search'); + + const duckduckgo = resolveServer({ id: 'duckduckgo', kind: 'duckduckgo' }); + assert.equal(duckduckgo.needsKey, false); + assert.equal(duckduckgo.toolName, 'duckduckgo_web_search'); + assert.equal(duckduckgo.countArg, 'count'); +}); + +test('provider result counts stay within upstream MCP schemas', () => { + assert.equal(clampSearchResults(resolveServer({ id: 't', kind: 'tavily' }), 1), 5); + assert.equal(clampSearchResults(resolveServer({ id: 't', kind: 'tavily' }), 50), 20); + assert.equal(clampSearchResults(resolveServer({ id: 'b', kind: 'brave' }), 50), 20); + assert.equal(clampSearchResults(resolveServer({ id: 'p', kind: 'perplexity' }), 0), 1); + assert.equal(clampSearchResults(resolveServer({ id: 'd', kind: 'duckduckgo' }), 50), 20); + assert.equal(clampSearchResults(resolveServer({ id: 'e', kind: 'exa' }), 50), 50); +}); +test('extractSearchResult normalizes, deduplicates, and rejects invalid URLs', () => { + const result = extractSearchResult({ + structuredContent: { + answer: 'Summary', + results: [ + { url: 'https://example.com/a', title: 'A', content: 'alpha', published_date: '2026-08-18' }, + { url: 'https://example.com/a', title: 'Duplicate' }, + { url: 'ftp://example.com/ignored', title: 'Ignored' }, + ], + nested: { url: 'http://example.com/b', description: 'beta' }, + }, + }); + + assert.deepEqual(result, { + sources: [ + { + url: 'https://example.com/a', + title: 'A', + snippet: 'alpha', + publishedAt: '2026-08-18', + }, + { url: 'http://example.com/b', snippet: 'beta' }, + ], + truncated: false, + content: 'Summary', + }); +}); + +test('extractSearchResult accepts JSON and plain text MCP blocks', () => { + const json = extractSearchResult({ + content: [{ type: 'text', text: '{"results":[{"url":"https://example.com"}]}' }], + }); + assert.equal(json.sources.length, 1); + + const text = extractSearchResult({ content: [{ type: 'text', text: 'Direct answer' }] }); + assert.deepEqual(text, { sources: [], truncated: false, content: 'Direct answer' }); +}); diff --git a/test/url-policy.test.js b/test/url-policy.test.js new file mode 100644 index 0000000..351b354 --- /dev/null +++ b/test/url-policy.test.js @@ -0,0 +1,158 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + createPinnedLookup, + isAllowedEndpointAddress, + isPublicAddress, + validateHttpEndpoint, +} from '../lib/url-policy.js'; + +const lookup = (records) => (_hostname, options, callback) => { + assert.equal(options.all, true); + queueMicrotask(() => callback(null, records)); +}; + +test('URL policy accepts HTTP(S) with public DNS only', async () => { + const result = await validateHttpEndpoint('https://search.example/mcp', { + lookup: lookup([ + { address: '8.8.8.8', family: 4 }, + { address: '2606:4700:4700:0:0:0:0:1111', family: 6 }, + ]), + }); + assert.equal(result.url.hostname, 'search.example'); + assert.deepEqual(result.addresses, [ + { address: '8.8.8.8', family: 4 }, + { address: '2606:4700:4700:0:0:0:0:1111', family: 6 }, + ]); +}); + +test('URL policy rejects schemes, userinfo, localhost, and ambiguous IPv4', async () => { + for (const input of [ + 'ftp://example.com/mcp', + 'https://user:pass@example.com/mcp', + 'http://localhost/mcp', + 'http://api.localhost/mcp', + 'http://127.0.0.1/mcp', + 'http://127.1/mcp', + 'http://0177.0.0.1/mcp', + 'http://0x7f000001/mcp', + ]) { + await assert.rejects(() => validateHttpEndpoint(input), { name: 'SearchMcpUrlPolicyError' }, input); + } +}); + +test('URL policy rejects private, reserved, test, mapped, and transition ranges', () => { + for (const address of [ + '0.0.0.0', + '10.0.0.1', + '100.64.0.1', + '127.0.0.1', + '169.254.169.254', + '172.16.0.1', + '192.168.0.1', + '192.0.2.1', + '198.18.0.1', + '198.51.100.1', + '203.0.113.1', + '224.0.0.1', + '255.255.255.255', + '::', + '::1', + '::ffff:127.0.0.1', + '::7f00:1', + '::a00:1', + '::a9fe:a9fe', + '::c0a8:101', + '::808:808', + '64:ff9b::808:808', + '2001:db8::1', + '2001::1', + '2002:0808:0808::1', + 'fc00::1', + 'fe80::1', + 'ff02::1', + ]) { + assert.equal(isPublicAddress(address), false, address); + } + assert.equal(isPublicAddress('8.8.8.8'), true); + assert.equal(isPublicAddress('2606:4700:4700::1111'), true); +}); + +test('URL policy drops private DNS answers and keeps public ones', async () => { + const result = await validateHttpEndpoint('https://search.example/mcp', { + lookup: lookup([ + { address: '8.8.8.8', family: 4 }, + { address: '10.0.0.1', family: 4 }, + ]), + }); + assert.deepEqual(result.addresses, [{ address: '8.8.8.8', family: 4 }]); +}); + +test('URL policy accepts Clash fake-IP answers with optional public peers', async () => { + const mixed = await validateHttpEndpoint('https://dashscope.example/mcp', { + lookup: lookup([ + { address: '198.18.2.146', family: 4 }, + { address: '2408:400a:3e:ef02:12f:bd95:e827:51d', family: 6 }, + ]), + }); + assert.deepEqual(mixed.addresses, [ + { address: '198.18.2.146', family: 4 }, + { address: '2408:400a:3e:ef02:12f:bd95:e827:51d', family: 6 }, + ]); + + const fakeOnly = await validateHttpEndpoint('https://dashscope.example/mcp', { + lookup: lookup([{ address: '198.18.2.146', family: 4 }]), + }); + assert.deepEqual(fakeOnly.addresses, [{ address: '198.18.2.146', family: 4 }]); + assert.equal(isAllowedEndpointAddress('198.18.2.146'), true); + assert.equal(isAllowedEndpointAddress('10.0.0.1'), false); +}); + +test('URL policy rejects private-only DNS answers', async () => { + await assert.rejects( + () => validateHttpEndpoint('https://search.example/mcp', { + lookup: lookup([{ address: '10.0.0.1', family: 4 }]), + }), + /non-public address/, + ); +}); + +test('URL policy rejects DNS errors and empty answers without leaking endpoint data', async () => { + await assert.rejects( + () => validateHttpEndpoint('https://search.example/mcp', { + lookup: (_hostname, _options, callback) => callback(new Error('resolver detail')), + }), + /resolution failed/, + ); + await assert.rejects( + () => validateHttpEndpoint('https://search.example/mcp', { lookup: lookup([]) }), + /did not resolve/, + ); +}); + +test('URL policy aborts a pending DNS lookup', async () => { + const controller = new AbortController(); + const pending = validateHttpEndpoint('https://search.example/mcp', { + lookup: () => {}, + signal: controller.signal, + }); + controller.abort(); + await assert.rejects(pending, /validation was aborted/); +}); + +test('pinned lookup serves only validated host and addresses', async () => { + const pinned = createPinnedLookup('search.example', [ + { address: '8.8.8.8', family: 4 }, + { address: '2606:4700:4700:0:0:0:0:1111', family: 6 }, + ]); + const all = await new Promise((resolve, reject) => { + pinned('search.example', { all: true }, (error, records) => error ? reject(error) : resolve(records)); + }); + assert.deepEqual(all, [ + { address: '8.8.8.8', family: 4 }, + { address: '2606:4700:4700:0:0:0:0:1111', family: 6 }, + ]); + await assert.rejects(new Promise((resolve, reject) => { + pinned('other.example', {}, (error, address) => error ? reject(error) : resolve(address)); + }), /unvalidated hostname/); +});