@harrylabsj/kiwi 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +1 -1
  2. package/dist/a2a/negotiate.d.ts +6 -2
  3. package/dist/a2a/negotiate.js +10 -3
  4. package/dist/a2a/negotiate.js.map +1 -1
  5. package/dist/a2a/node.d.ts +4 -1
  6. package/dist/a2a/node.js +56 -3
  7. package/dist/a2a/node.js.map +1 -1
  8. package/dist/a2a/server/merchant-handler.d.ts +11 -3
  9. package/dist/a2a/server/merchant-handler.js +84 -26
  10. package/dist/a2a/server/merchant-handler.js.map +1 -1
  11. package/dist/agent/buyer/buyer-tools.js +2 -2
  12. package/dist/agent/buyer/buyer-tools.js.map +1 -1
  13. package/dist/agent/kernel.js +5 -1
  14. package/dist/agent/kernel.js.map +1 -1
  15. package/dist/agent/memory/schema.d.ts +1 -1
  16. package/dist/agent/memory/schema.js +9 -1
  17. package/dist/agent/memory/schema.js.map +1 -1
  18. package/dist/agent/merchant/action-candidate.d.ts +18 -0
  19. package/dist/agent/merchant/action-candidate.js +51 -0
  20. package/dist/agent/merchant/action-candidate.js.map +1 -1
  21. package/dist/agent/merchant/fake-merchant-client.js +6 -1
  22. package/dist/agent/merchant/fake-merchant-client.js.map +1 -1
  23. package/dist/agent/merchant/intelligence/default-backend.js +25 -2
  24. package/dist/agent/merchant/intelligence/default-backend.js.map +1 -1
  25. package/dist/agent/merchant/merchant-client.d.ts +50 -1
  26. package/dist/agent/merchant/merchant-client.js +157 -8
  27. package/dist/agent/merchant/merchant-client.js.map +1 -1
  28. package/dist/agent/merchant/merchant-presentations.js +25 -2
  29. package/dist/agent/merchant/merchant-presentations.js.map +1 -1
  30. package/dist/agent/merchant/merchant-tools.js +102 -188
  31. package/dist/agent/merchant/merchant-tools.js.map +1 -1
  32. package/dist/agent/merchant/types.d.ts +6 -2
  33. package/dist/agent/merchant/types.js +45 -0
  34. package/dist/agent/merchant/types.js.map +1 -1
  35. package/dist/auth/merchant-authorization.d.ts +49 -0
  36. package/dist/auth/merchant-authorization.js +61 -0
  37. package/dist/auth/merchant-authorization.js.map +1 -0
  38. package/dist/auth/merchant-oauth.d.ts +255 -0
  39. package/dist/auth/merchant-oauth.js +764 -0
  40. package/dist/auth/merchant-oauth.js.map +1 -0
  41. package/dist/auth/merchant-pairing.d.ts +62 -0
  42. package/dist/auth/merchant-pairing.js +199 -0
  43. package/dist/auth/merchant-pairing.js.map +1 -0
  44. package/dist/auth/merchant-sessions.d.ts +71 -0
  45. package/dist/auth/merchant-sessions.js +168 -0
  46. package/dist/auth/merchant-sessions.js.map +1 -0
  47. package/dist/buyer-core/build-service.d.ts +5 -0
  48. package/dist/buyer-core/build-service.js +12 -0
  49. package/dist/buyer-core/build-service.js.map +1 -1
  50. package/dist/buyer-core/errors.d.ts +1 -1
  51. package/dist/buyer-core/errors.js +2 -0
  52. package/dist/buyer-core/errors.js.map +1 -1
  53. package/dist/buyer-core/merchant-index.d.ts +19 -4
  54. package/dist/buyer-core/merchant-index.js +137 -16
  55. package/dist/buyer-core/merchant-index.js.map +1 -1
  56. package/dist/buyer-core/negotiator.d.ts +0 -10
  57. package/dist/buyer-core/negotiator.js +12 -1
  58. package/dist/buyer-core/negotiator.js.map +1 -1
  59. package/dist/buyer-core/quote-fetcher.js +2 -1
  60. package/dist/buyer-core/quote-fetcher.js.map +1 -1
  61. package/dist/buyer-core/service.d.ts +104 -0
  62. package/dist/buyer-core/service.js +105 -1
  63. package/dist/buyer-core/service.js.map +1 -1
  64. package/dist/cli.d.ts +4 -2
  65. package/dist/cli.js +612 -20
  66. package/dist/cli.js.map +1 -1
  67. package/dist/config/profile.d.ts +30 -0
  68. package/dist/config/profile.js +126 -61
  69. package/dist/config/profile.js.map +1 -1
  70. package/dist/demo/demo-runner.js +12 -9
  71. package/dist/demo/demo-runner.js.map +1 -1
  72. package/dist/discovery/catalog-source/buyer-follows.d.ts +82 -0
  73. package/dist/discovery/catalog-source/buyer-follows.js +228 -0
  74. package/dist/discovery/catalog-source/buyer-follows.js.map +1 -0
  75. package/dist/discovery/catalog-source/connector-identity.d.ts +75 -0
  76. package/dist/discovery/catalog-source/connector-identity.js +175 -0
  77. package/dist/discovery/catalog-source/connector-identity.js.map +1 -0
  78. package/dist/discovery/catalog-source/errors.d.ts +1 -1
  79. package/dist/discovery/catalog-source/errors.js +2 -0
  80. package/dist/discovery/catalog-source/errors.js.map +1 -1
  81. package/dist/discovery/catalog-source/merchant-publications.d.ts +65 -0
  82. package/dist/discovery/catalog-source/merchant-publications.js +207 -0
  83. package/dist/discovery/catalog-source/merchant-publications.js.map +1 -0
  84. package/dist/discovery/catalog-source/register.d.ts +24 -0
  85. package/dist/discovery/catalog-source/register.js +46 -0
  86. package/dist/discovery/catalog-source/register.js.map +1 -1
  87. package/dist/discovery/catalog-source/source.js +2 -1
  88. package/dist/discovery/catalog-source/source.js.map +1 -1
  89. package/dist/fs/atomic-write.d.ts +18 -0
  90. package/dist/fs/atomic-write.js +29 -0
  91. package/dist/fs/atomic-write.js.map +1 -0
  92. package/dist/http/server.js +4 -1
  93. package/dist/http/server.js.map +1 -1
  94. package/dist/mcp/cli.d.ts +2 -0
  95. package/dist/mcp/cli.js +7 -6
  96. package/dist/mcp/cli.js.map +1 -1
  97. package/dist/mcp/merchant-auth.d.ts +78 -0
  98. package/dist/mcp/merchant-auth.js +146 -0
  99. package/dist/mcp/merchant-auth.js.map +1 -0
  100. package/dist/mcp/merchant-dirs.d.ts +26 -0
  101. package/dist/mcp/merchant-dirs.js +44 -0
  102. package/dist/mcp/merchant-dirs.js.map +1 -0
  103. package/dist/mcp/merchant-resources.d.ts +38 -0
  104. package/dist/mcp/merchant-resources.js +146 -0
  105. package/dist/mcp/merchant-resources.js.map +1 -0
  106. package/dist/mcp/merchant-server.d.ts +106 -0
  107. package/dist/mcp/merchant-server.js +615 -0
  108. package/dist/mcp/merchant-server.js.map +1 -0
  109. package/dist/mcp/merchant-tools.d.ts +103 -0
  110. package/dist/mcp/merchant-tools.js +517 -0
  111. package/dist/mcp/merchant-tools.js.map +1 -0
  112. package/dist/mcp/server.js +4 -3
  113. package/dist/mcp/server.js.map +1 -1
  114. package/dist/mcp/tools.d.ts +5 -3
  115. package/dist/mcp/tools.js +102 -5
  116. package/dist/mcp/tools.js.map +1 -1
  117. package/dist/mcp/types.d.ts +1 -1
  118. package/dist/mcp/types.js +4 -0
  119. package/dist/mcp/types.js.map +1 -1
  120. package/dist/merchant/ops.d.ts +0 -11
  121. package/dist/merchant/ops.js +13 -1
  122. package/dist/merchant/ops.js.map +1 -1
  123. package/dist/merchant/workbench-service.d.ts +189 -0
  124. package/dist/merchant/workbench-service.js +417 -0
  125. package/dist/merchant/workbench-service.js.map +1 -0
  126. package/dist/merchant-admin/pending-page.d.ts +35 -0
  127. package/dist/merchant-admin/pending-page.js +69 -0
  128. package/dist/merchant-admin/pending-page.js.map +1 -0
  129. package/dist/merchant-core/commands.d.ts +99 -0
  130. package/dist/merchant-core/commands.js +179 -0
  131. package/dist/merchant-core/commands.js.map +1 -0
  132. package/dist/merchant-core/executor.d.ts +69 -0
  133. package/dist/merchant-core/executor.js +218 -0
  134. package/dist/merchant-core/executor.js.map +1 -0
  135. package/dist/merchant-core/negotiation-adapters.d.ts +55 -0
  136. package/dist/merchant-core/negotiation-adapters.js +55 -0
  137. package/dist/merchant-core/negotiation-adapters.js.map +1 -0
  138. package/dist/merchant-core/network-checks.d.ts +65 -0
  139. package/dist/merchant-core/network-checks.js +111 -0
  140. package/dist/merchant-core/network-checks.js.map +1 -0
  141. package/dist/merchant-core/operations.d.ts +64 -0
  142. package/dist/merchant-core/operations.js +136 -0
  143. package/dist/merchant-core/operations.js.map +1 -0
  144. package/dist/merchant-core/policy-runtime.d.ts +70 -0
  145. package/dist/merchant-core/policy-runtime.js +181 -0
  146. package/dist/merchant-core/policy-runtime.js.map +1 -0
  147. package/dist/merchant-core/product-import.d.ts +67 -0
  148. package/dist/merchant-core/product-import.js +160 -0
  149. package/dist/merchant-core/product-import.js.map +1 -0
  150. package/dist/merchant-core/service.d.ts +203 -0
  151. package/dist/merchant-core/service.js +402 -0
  152. package/dist/merchant-core/service.js.map +1 -0
  153. package/dist/merchant-gateway/catalog-publications.d.ts +71 -0
  154. package/dist/merchant-gateway/catalog-publications.js +175 -0
  155. package/dist/merchant-gateway/catalog-publications.js.map +1 -0
  156. package/dist/merchant-gateway/catalog-tools.d.ts +46 -0
  157. package/dist/merchant-gateway/catalog-tools.js +277 -0
  158. package/dist/merchant-gateway/catalog-tools.js.map +1 -0
  159. package/dist/merchant-gateway/cli.d.ts +75 -0
  160. package/dist/merchant-gateway/cli.js +458 -0
  161. package/dist/merchant-gateway/cli.js.map +1 -0
  162. package/dist/merchant-gateway/credential-vault.d.ts +44 -0
  163. package/dist/merchant-gateway/credential-vault.js +111 -0
  164. package/dist/merchant-gateway/credential-vault.js.map +1 -0
  165. package/dist/merchant-gateway/entry-server.d.ts +117 -0
  166. package/dist/merchant-gateway/entry-server.js +710 -0
  167. package/dist/merchant-gateway/entry-server.js.map +1 -0
  168. package/dist/merchant-gateway/instance-registration.d.ts +143 -0
  169. package/dist/merchant-gateway/instance-registration.js +382 -0
  170. package/dist/merchant-gateway/instance-registration.js.map +1 -0
  171. package/dist/merchant-gateway/instance-tools.d.ts +58 -0
  172. package/dist/merchant-gateway/instance-tools.js +184 -0
  173. package/dist/merchant-gateway/instance-tools.js.map +1 -0
  174. package/dist/merchant-gateway/mcp-proxy.d.ts +25 -0
  175. package/dist/merchant-gateway/mcp-proxy.js +192 -0
  176. package/dist/merchant-gateway/mcp-proxy.js.map +1 -0
  177. package/dist/merchant-gateway/pinned-fetch.d.ts +52 -0
  178. package/dist/merchant-gateway/pinned-fetch.js +212 -0
  179. package/dist/merchant-gateway/pinned-fetch.js.map +1 -0
  180. package/dist/merchant-gateway/tenant-registry.d.ts +82 -0
  181. package/dist/merchant-gateway/tenant-registry.js +190 -0
  182. package/dist/merchant-gateway/tenant-registry.js.map +1 -0
  183. package/dist/merchant-gateway/tool-bundle.d.ts +26 -0
  184. package/dist/merchant-gateway/tool-bundle.js +56 -0
  185. package/dist/merchant-gateway/tool-bundle.js.map +1 -0
  186. package/dist/merchant-runtime/backup.d.ts +48 -0
  187. package/dist/merchant-runtime/backup.js +226 -0
  188. package/dist/merchant-runtime/backup.js.map +1 -0
  189. package/dist/merchant-runtime/health.d.ts +99 -0
  190. package/dist/merchant-runtime/health.js +206 -0
  191. package/dist/merchant-runtime/health.js.map +1 -0
  192. package/dist/merchant-runtime/jobs.d.ts +64 -0
  193. package/dist/merchant-runtime/jobs.js +107 -0
  194. package/dist/merchant-runtime/jobs.js.map +1 -0
  195. package/dist/merchant-runtime/manager.d.ts +94 -0
  196. package/dist/merchant-runtime/manager.js +305 -0
  197. package/dist/merchant-runtime/manager.js.map +1 -0
  198. package/dist/merchant-runtime/services.d.ts +30 -0
  199. package/dist/merchant-runtime/services.js +51 -0
  200. package/dist/merchant-runtime/services.js.map +1 -0
  201. package/dist/net/url.d.ts +26 -0
  202. package/dist/net/url.js +32 -0
  203. package/dist/net/url.js.map +1 -0
  204. package/dist/product-cli.js +22 -3
  205. package/dist/product-cli.js.map +1 -1
  206. package/dist/product-compat.d.ts +9 -1
  207. package/dist/product-compat.js +12 -1
  208. package/dist/product-compat.js.map +1 -1
  209. package/dist/product-init.js +17 -5
  210. package/dist/product-init.js.map +1 -1
  211. package/dist/product-publish.js +2 -1
  212. package/dist/product-publish.js.map +1 -1
  213. package/package.json +3 -2
  214. package/skills/kiwi-buyer/SKILL.md +2 -0
@@ -0,0 +1,710 @@
1
+ /**
2
+ * Copyright 2026 harrylabsj
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * 商家连接器(「Kiwi 商家运营」)的远程 MCP 入口。
18
+ *
19
+ * merchant-buddy 第 1 版设计 §1.6 / §4「两个连接器」与商家连接器发布计划 §2:
20
+ * 买方连接器(`kiwi-sourcing`,本地 stdio)保持不变;商家 Buddy 使用本入口,
21
+ * 服务端连接共享 `kiwi-catalog`、按**已验证主体**提供第 0 版目录能力,并按
22
+ * 服务端注册表路由到第 1 版的商家专属实例。
23
+ *
24
+ * 只服务商家侧:本入口不承载买方工具,也不复用 `kiwi-sourcing` 的身份或回调
25
+ * (两者 source、平台 ID、凭据互不通用)。本文件只负责入口的 HTTP 面
26
+ * (OAuth + 连接流程 + MCP 传输),工具集合与租户路由由注入的 provider 决定。
27
+ *
28
+ * 身份建立(本版实现):
29
+ *
30
+ * ```
31
+ * WorkBuddy --GET /oauth/authorize--> 入口(无会话)
32
+ * 303 /connect?next=<原 authorize URL>
33
+ * → 创建 catalog 一次性授权请求(connector token)
34
+ * 303 <catalog login_url>(商家在 Kiwi 页面登录/注册 + 确认)
35
+ * → catalog 303 /connect/callback?resume=<原 authorize URL>&request_id&code
36
+ * → 服务端兑换 {merchant_id, merchant_name} → 建会话 cookie
37
+ * 303 <原 authorize URL>(此时有会话 → 授权同意页 → 授权码回跳 WorkBuddy)
38
+ * ```
39
+ *
40
+ * 硬边界:
41
+ * - `merchant_id` 只来自 catalog 兑换结果,绝不来自工具参数/URL/模型输出;
42
+ * - 会话是 HttpOnly cookie,目录密码与一次性 code 都不进入 token 或日志;
43
+ * - `next` / `resume` 只接受站内相对路径(防开放重定向)。
44
+ */
45
+ import { readFileSync } from "node:fs";
46
+ import { createServer, } from "node:http";
47
+ import { createServer as createSecureServer } from "node:https";
48
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
49
+ import { ADMIN_SESSION_COOKIE } from "../auth/merchant-sessions.js";
50
+ import { createProtocolServer } from "../mcp/merchant-server.js";
51
+ import { bindInstance, bindInstanceViaPairing, unbindInstance, } from "./instance-registration.js";
52
+ export const DEFAULT_GATEWAY_ENTRY_PORT = 9200;
53
+ export const DEFAULT_GATEWAY_ENTRY_PATH = "/mcp";
54
+ const MAX_BODY_BYTES = 1_048_576;
55
+ const NO_STORE_HEADERS = {
56
+ "cache-control": "no-store",
57
+ pragma: "no-cache",
58
+ };
59
+ function writeJson(res, status, body, headers = {}) {
60
+ res.writeHead(status, { "content-type": "application/json", ...headers });
61
+ res.end(JSON.stringify(body));
62
+ }
63
+ function writeHtml(res, status, html) {
64
+ res.writeHead(status, {
65
+ "content-type": "text/html; charset=utf-8",
66
+ ...NO_STORE_HEADERS,
67
+ "x-content-type-options": "nosniff",
68
+ });
69
+ res.end(html);
70
+ }
71
+ /** 只接受站内相对路径(拒绝协议相对 URL 与反斜杠变体,防开放重定向)。 */
72
+ export function safeRelativePath(value) {
73
+ if (value === undefined)
74
+ return undefined;
75
+ const path = value.trim();
76
+ if (!path.startsWith("/") || path.startsWith("//"))
77
+ return undefined;
78
+ if (path.includes("\\") || path.includes("\n") || path.includes("\r"))
79
+ return undefined;
80
+ return path;
81
+ }
82
+ /** 连接流程的回跳目标必须是入口自己的 authorize 端点。 */
83
+ function safeResume(value) {
84
+ const path = safeRelativePath(value);
85
+ if (path === undefined)
86
+ return undefined;
87
+ if (path !== "/oauth/authorize" && !path.startsWith("/oauth/authorize?"))
88
+ return undefined;
89
+ return path;
90
+ }
91
+ /**
92
+ * 二次同源校验:把相对路径按本服务 origin 解析一次,只有解析结果仍落在本服务时
93
+ * 才作为 `Location` 使用(否则回落到 `/`)。
94
+ *
95
+ * `safeResume` 已把取值限定为 `/oauth/authorize` 前缀,这里是纵深防御:将来若
96
+ * 放宽前缀白名单,也不会退化成开放重定向。
97
+ */
98
+ function sameOriginPath(path, origin) {
99
+ try {
100
+ const target = new URL(path, origin);
101
+ if (target.origin !== origin)
102
+ return "/";
103
+ return `${target.pathname}${target.search}`;
104
+ }
105
+ catch {
106
+ return "/";
107
+ }
108
+ }
109
+ function cookieValue(req, name) {
110
+ const header = req.headers.cookie;
111
+ if (header === undefined)
112
+ return undefined;
113
+ for (const part of header.split(";")) {
114
+ const eq = part.indexOf("=");
115
+ if (eq === -1)
116
+ continue;
117
+ if (part.slice(0, eq).trim() === name)
118
+ return part.slice(eq + 1).trim();
119
+ }
120
+ return undefined;
121
+ }
122
+ async function readBody(req) {
123
+ const chunks = [];
124
+ let size = 0;
125
+ for await (const chunk of req) {
126
+ size += chunk.length;
127
+ if (size > MAX_BODY_BYTES)
128
+ throw new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`);
129
+ chunks.push(chunk);
130
+ }
131
+ if (chunks.length === 0)
132
+ return undefined;
133
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
134
+ }
135
+ async function readForm(req) {
136
+ const chunks = [];
137
+ let size = 0;
138
+ for await (const chunk of req) {
139
+ size += chunk.length;
140
+ if (size > MAX_BODY_BYTES)
141
+ throw new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`);
142
+ chunks.push(chunk);
143
+ }
144
+ const text = Buffer.concat(chunks).toString("utf8");
145
+ if (text.trim() === "")
146
+ return {};
147
+ if ((req.headers["content-type"] ?? "").includes("application/json")) {
148
+ return JSON.parse(text);
149
+ }
150
+ return Object.fromEntries(new URLSearchParams(text).entries());
151
+ }
152
+ function withQueryParam(target, key, value) {
153
+ const sep = target.includes("?") ? "&" : "?";
154
+ return `${target}${sep}${new URLSearchParams({ [key]: value }).toString()}`;
155
+ }
156
+ /**
157
+ * 启动商家连接器入口。返回 handle 含实际 host/port 与优雅关闭。
158
+ */
159
+ export async function startGatewayEntryServer(options) {
160
+ const host = options.host ?? "127.0.0.1";
161
+ const port = options.port ?? DEFAULT_GATEWAY_ENTRY_PORT;
162
+ const mcpPath = options.mcpPath ?? DEFAULT_GATEWAY_ENTRY_PATH;
163
+ const serverInfo = options.serverInfo ?? { name: "kiwi-merchant-entry", version: "0.0.0" };
164
+ const publicBaseUrl = options.publicBaseUrl.replace(/\/+$/, "");
165
+ const clientLabel = options.clientLabel ?? "Kiwi 商家运营";
166
+ let closing = false;
167
+ const transports = new Set();
168
+ const writeOAuthResult = (res, result) => {
169
+ const headers = { ...NO_STORE_HEADERS, ...(result.headers ?? {}) };
170
+ if (result.html !== undefined) {
171
+ writeHtml(res, result.status, result.html);
172
+ return;
173
+ }
174
+ if (headers.location !== undefined) {
175
+ res.writeHead(result.status, headers);
176
+ res.end();
177
+ return;
178
+ }
179
+ writeJson(res, result.status, result.body ?? {}, headers);
180
+ };
181
+ const sessionFor = (req) => {
182
+ const sessionId = cookieValue(req, ADMIN_SESSION_COOKIE);
183
+ if (sessionId === undefined)
184
+ return undefined;
185
+ return options.sessions.getSession(sessionId);
186
+ };
187
+ /**
188
+ * 开始连接:向 catalog 申请一次性身份授权,把商家浏览器送到目录登录页。
189
+ * resume 是入口自己的 authorize 相对路径,连接完成后原样回到该授权请求。
190
+ */
191
+ const startConnect = async (res, resume) => {
192
+ const returnUrl = withQueryParam(`${publicBaseUrl}/connect/callback`, "resume", resume);
193
+ let request;
194
+ try {
195
+ request = await options.identity.createRequest({ returnUrl, clientLabel });
196
+ }
197
+ catch (err) {
198
+ // 目录不可达:明确失败页(不假装已连接,也不把错误细节暴露给浏览器)。
199
+ process.stderr.write(`[gateway entry] connector identity request failed: ${err instanceof Error ? err.message : String(err)}\n`);
200
+ writeHtml(res, 502, `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>连接暂不可用</title></head><body>
201
+ <h1>暂时无法开始 Kiwi 目录连接</h1>
202
+ <p>请稍后重试;如果你还没有商家账号,可以在重试时于 Kiwi 页面注册并验证邮箱。</p>
203
+ </body></html>`);
204
+ return;
205
+ }
206
+ res.writeHead(303, { location: request.loginUrl, ...NO_STORE_HEADERS });
207
+ res.end();
208
+ };
209
+ /** GET /connect/callback:目录回跳(携带一次性 code 或 access_denied)。 */
210
+ const handleConnectCallback = async (res, url) => {
211
+ const resumeParam = safeResume(url.searchParams.get("resume") ?? undefined);
212
+ if (resumeParam === undefined) {
213
+ writeHtml(res, 400, `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>连接参数无效</title></head><body>
214
+ <h1>连接参数无效</h1><p>请返回 Buddy 重新发起连接。</p></body></html>`);
215
+ return;
216
+ }
217
+ const resume = sameOriginPath(resumeParam, url.origin);
218
+ const denied = url.searchParams.get("error");
219
+ if (denied !== null) {
220
+ // 商家在目录拒绝(或目录返回错误):按 OAuth 语义把失败带回客户端。
221
+ res.writeHead(303, {
222
+ location: withQueryParam(resume, "connect", denied === "access_denied" ? "denied" : "failed"),
223
+ ...NO_STORE_HEADERS,
224
+ });
225
+ res.end();
226
+ return;
227
+ }
228
+ const requestId = url.searchParams.get("request_id") ?? "";
229
+ const code = url.searchParams.get("code") ?? "";
230
+ let identity;
231
+ try {
232
+ identity = await options.identity.exchange({ requestId, code });
233
+ }
234
+ catch (err) {
235
+ process.stderr.write(`[gateway entry] connector identity exchange failed: ${err instanceof Error ? err.message : String(err)}\n`);
236
+ res.writeHead(303, {
237
+ location: withQueryParam(resume, "connect", "failed"),
238
+ ...NO_STORE_HEADERS,
239
+ });
240
+ res.end();
241
+ return;
242
+ }
243
+ // 目录商家凭据入库(加密保管):入口以此代表该商家调用目录商家接口。
244
+ // 存储失败不阻断连接(工具会在缺凭据时明确报「需要重新连接」)。
245
+ if (options.credentials !== undefined) {
246
+ try {
247
+ options.credentials.put(identity.merchantId, identity.credential.accessToken, identity.credential.expiresAt);
248
+ }
249
+ catch (err) {
250
+ process.stderr.write(`[gateway entry] credential vault write failed: ${err instanceof Error ? err.message : String(err)}\n`);
251
+ }
252
+ }
253
+ // 会话主体:principal 取商家身份(目录 account 已确认过的 merchant_id 归属)。
254
+ const session = options.sessions.createSession({
255
+ principalId: `merchant:${identity.merchantId}`,
256
+ merchantId: identity.merchantId,
257
+ });
258
+ const secure = options.secureCookies === true ? "; Secure" : "";
259
+ res.writeHead(303, {
260
+ location: resume,
261
+ "set-cookie": `${ADMIN_SESSION_COOKIE}=${session.sessionId}; HttpOnly; SameSite=Lax; Path=/${secure}`,
262
+ ...NO_STORE_HEADERS,
263
+ });
264
+ res.end();
265
+ };
266
+ const routeOAuth = async (req, res, url) => {
267
+ const oauth = options.oauth;
268
+ const p = url.pathname;
269
+ if (req.method === "GET" && p === "/.well-known/oauth-protected-resource") {
270
+ writeOAuthResult(res, oauth.protectedResourceMetadata());
271
+ return true;
272
+ }
273
+ if (req.method === "GET" && p === "/.well-known/oauth-authorization-server") {
274
+ writeOAuthResult(res, oauth.authorizationServerMetadata());
275
+ return true;
276
+ }
277
+ if (req.method === "POST" && p === "/oauth/register") {
278
+ let body;
279
+ try {
280
+ body = await readBody(req);
281
+ }
282
+ catch {
283
+ writeJson(res, 400, {
284
+ error: "invalid_client_metadata",
285
+ error_description: "request body must be valid JSON",
286
+ });
287
+ return true;
288
+ }
289
+ writeOAuthResult(res, oauth.register(body));
290
+ return true;
291
+ }
292
+ if (req.method === "GET" && p === "/oauth/authorize") {
293
+ const query = Object.fromEntries(url.searchParams.entries());
294
+ const session = sessionFor(req);
295
+ if (session === undefined) {
296
+ // 无会话:先走目录连接(loginPath 在 CLI 侧配置为 /connect)。
297
+ const next = `/oauth/authorize?${new URLSearchParams(Object.entries(query).filter((e) => e[1] !== undefined)).toString()}`;
298
+ // 连接失败/被拒绝的标记只用于回跳语义,不进入 OAuth 参数。
299
+ const connect = query.connect;
300
+ delete query.connect;
301
+ if (connect === "denied" || connect === "failed") {
302
+ writeOAuthResult(res, oauth.authorizationError(query, connect === "denied" ? "access_denied" : "server_error", connect === "denied"
303
+ ? "商家未在 Kiwi 目录完成连接"
304
+ : "商家身份连接失败,请返回应用重试"));
305
+ return true;
306
+ }
307
+ writeOAuthResult(res, {
308
+ status: 303,
309
+ headers: { location: `/connect?next=${encodeURIComponent(next)}` },
310
+ });
311
+ return true;
312
+ }
313
+ writeOAuthResult(res, oauth.authorize(query, session));
314
+ return true;
315
+ }
316
+ if (req.method === "POST" && p === "/oauth/authorize") {
317
+ let form;
318
+ try {
319
+ form = await readForm(req);
320
+ }
321
+ catch {
322
+ writeJson(res, 400, { error: "invalid_request", error_description: "malformed body" });
323
+ return true;
324
+ }
325
+ const session = sessionFor(req);
326
+ if (session === undefined) {
327
+ writeJson(res, 401, { error: "login_required", error_description: "授权提交需要已连接的商家会话" }, NO_STORE_HEADERS);
328
+ return true;
329
+ }
330
+ writeOAuthResult(res, oauth.authorizeSubmit(form, session));
331
+ return true;
332
+ }
333
+ if (req.method === "POST" && p === "/oauth/token") {
334
+ let form;
335
+ try {
336
+ form = await readForm(req);
337
+ }
338
+ catch {
339
+ writeJson(res, 400, { error: "invalid_request", error_description: "malformed body" });
340
+ return true;
341
+ }
342
+ writeOAuthResult(res, oauth.token(form));
343
+ return true;
344
+ }
345
+ if (req.method === "POST" && p === "/oauth/revoke") {
346
+ let form;
347
+ try {
348
+ form = await readForm(req);
349
+ }
350
+ catch {
351
+ writeJson(res, 400, { error: "invalid_request", error_description: "malformed body" });
352
+ return true;
353
+ }
354
+ writeOAuthResult(res, oauth.revoke(form));
355
+ return true;
356
+ }
357
+ if (p.startsWith("/oauth/") || p.startsWith("/.well-known/oauth-")) {
358
+ writeJson(res, 404, { error: "not_found", message: `unknown OAuth endpoint ${p}` });
359
+ return true;
360
+ }
361
+ return false;
362
+ };
363
+ /** 连接入口(OAuth loginPath):有会话直接回到授权请求,否则开始目录连接。 */
364
+ const handleConnect = (res, url) => {
365
+ const next = safeRelativePath(url.searchParams.get("next") ?? undefined);
366
+ if (next === undefined) {
367
+ writeHtml(res, 400, `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>连接参数无效</title></head><body>
368
+ <h1>连接参数无效</h1><p>请返回 Buddy 重新发起连接。</p></body></html>`);
369
+ return;
370
+ }
371
+ void startConnect(res, next);
372
+ };
373
+ const escapeHtml = (text) => text
374
+ .replaceAll("&", "&amp;")
375
+ .replaceAll("<", "&lt;")
376
+ .replaceAll(">", "&gt;")
377
+ .replaceAll('"', "&quot;");
378
+ /** `/instance` 页面:展示当前绑定 + 绑定/解绑表单(需商家会话)。 */
379
+ const renderInstancePage = (registration, notice, merchantId) => {
380
+ const bound = registration !== undefined;
381
+ const noticeHtml = notice === undefined
382
+ ? ""
383
+ : `<p class="${notice.kind === "ok" ? "ok" : "err"}">${escapeHtml(notice.text)}</p>`;
384
+ const version = bound
385
+ ? registration.instanceVersion === ""
386
+ ? "未上报(实例版本较旧;建议升级以获得配对码与凭据轮换)"
387
+ : `${escapeHtml(registration.instanceName || "kiwi-merchant")} v${escapeHtml(registration.instanceVersion)}`
388
+ : "";
389
+ const bindingNote = bound
390
+ ? ` 连接方式:${registration.boundVia === "pairing" ? "一次性配对码" : registration.boundVia === "paste" ? "粘贴内部令牌" : "未知"}`
391
+ : "";
392
+ const current = bound
393
+ ? `<p>当前已绑定实例:<code>${escapeHtml(registration.mcpUrl)}</code>(更新于 ${escapeHtml(registration.updatedAt)})</p>
394
+ <p>实例版本:${version}${bindingNote}</p>
395
+ <p>可用工具:${registration.toolCount} 个(绑定当时探测;后续按实例实际能力动态刷新)</p>
396
+ <form method="post" action="/instance/unbind">
397
+ <button type="submit">解除绑定</button>
398
+ </form>`
399
+ : `<p>尚未绑定自有 Kiwi Merchant 实例。未绑定时第 0 版目录能力照常可用。</p>`;
400
+ return `<!doctype html>
401
+ <html lang="zh-CN">
402
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
403
+ <title>连接我的 Kiwi Merchant 服务</title>
404
+ <style>
405
+ body { font-family: system-ui, -apple-system, "PingFang SC", sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; line-height: 1.7; color: #1a1f1a; }
406
+ h1 { font-size: 1.4rem; } code { background: #f2f5f2; padding: 2px 6px; border-radius: 4px; }
407
+ .card { border: 1px solid #dde5dd; border-radius: 12px; padding: 20px; margin: 18px 0; }
408
+ .ok { color: #1b5e20; } .err { color: #b3261e; }
409
+ label { display: block; font-weight: 600; margin: 12px 0 4px; font-size: 0.92rem; }
410
+ input { width: 100%; padding: 10px 12px; border: 1px solid #dde5dd; border-radius: 8px; font-size: 0.95rem; }
411
+ button { margin-top: 14px; padding: 10px 20px; border: none; border-radius: 999px; background: #1b5e20; color: #fff; font-weight: 600; cursor: pointer; }
412
+ .small { color: #4b554b; font-size: 0.88rem; }
413
+ </style></head>
414
+ <body>
415
+ <h1>连接我的 Kiwi Merchant 服务</h1>
416
+ <p class="small">已连接商家:<code>${escapeHtml(merchantId)}</code></p>
417
+ ${noticeHtml}
418
+ <div class="card">
419
+ ${current}
420
+ </div>
421
+ <div class="card">
422
+ <h2 style="font-size:1.05rem;margin:0 0 6px">${bound ? "更换或重新绑定实例" : "绑定我的实例"}</h2>
423
+ <p class="small">实例需由你自己部署并保持运行(<code>kiwi merchant mcp serve</code>),
424
+ 公网地址必须为 HTTPS;同机部署可用 <code>http://127.0.0.1:&lt;端口&gt;/mcp</code>。
425
+ 内部令牌即实例的 <code>KIWI_MERCHANT_MCP_TOKEN</code>;本页不会回显令牌,网关会加密保存。</p>
426
+ <form method="post" action="/instance/bind">
427
+ <label for="mcp_url">实例 MCP 地址</label>
428
+ <input id="mcp_url" name="mcp_url" placeholder="https://your-merchant.example/mcp" autocomplete="off">
429
+ <label for="token">内部令牌(KIWI_MERCHANT_MCP_TOKEN)</label>
430
+ <input id="token" name="token" type="password" autocomplete="new-password">
431
+ <button type="submit">绑定并探活</button>
432
+ </form>
433
+ <p class="small">绑定前网关会带该令牌调用实例的 initialize 与 tools/list:
434
+ 地址可达、令牌正确、确为 MCP 实例,三者同时成立才会保存。</p>
435
+ </div>
436
+ <div class="card">
437
+ <h2 style="font-size:1.05rem;margin:0 0 6px">用一次性配对码绑定(推荐)</h2>
438
+ <p class="small">在实例所在机器上执行 <code>kiwi merchant mcp pair</code>,得到一次性配对码
439
+ (约 10 分钟有效、用完即失效)。这样无需把长期内部令牌填进表单。</p>
440
+ <form method="post" action="/instance/pair">
441
+ <label for="pair_url">实例 MCP 地址</label>
442
+ <input id="pair_url" name="mcp_url" placeholder="https://your-merchant.example/mcp" autocomplete="off">
443
+ <label for="pair_code">配对码</label>
444
+ <input id="pair_code" name="code" placeholder="XXXX-XXXX-XXXX" autocomplete="off">
445
+ <button type="submit">兑换并绑定</button>
446
+ </form>
447
+ <p class="small">网关会用它向实例兑换内部凭据(由实例侧生成,网关不签发),兑换成功后配对码立即作废。</p>
448
+ </div>
449
+ </body></html>`;
450
+ };
451
+ const handleInstancePage = async (req, res, url) => {
452
+ const binding = options.instanceBinding;
453
+ if (binding === undefined) {
454
+ writeJson(res, 404, { error: "not_found", message: "未启用实例绑定" });
455
+ return;
456
+ }
457
+ const session = sessionFor(req);
458
+ if (session === undefined) {
459
+ // 复用连接流程建立会话后回到本页。
460
+ res.writeHead(303, {
461
+ location: `/connect?next=${encodeURIComponent("/instance")}`,
462
+ ...NO_STORE_HEADERS,
463
+ });
464
+ res.end();
465
+ return;
466
+ }
467
+ const merchantId = session.merchant_id;
468
+ const noticeFromQuery = () => {
469
+ const kind = url.searchParams.get("status");
470
+ const error = url.searchParams.get("error");
471
+ if (error !== null)
472
+ return { kind: "error", text: error };
473
+ if (kind === "bound")
474
+ return { kind: "ok", text: "实例已绑定(已探活通过)。" };
475
+ if (kind === "paired")
476
+ return { kind: "ok", text: "实例已用配对码绑定(凭据已加密保存)。" };
477
+ if (kind === "unbound")
478
+ return { kind: "ok", text: "实例已解绑;目录公开资料与买家关注不受影响。" };
479
+ return undefined;
480
+ };
481
+ if (req.method === "GET") {
482
+ const registration = binding.registrations.get(merchantId);
483
+ writeHtml(res, 200, renderInstancePage(registration === undefined
484
+ ? undefined
485
+ : {
486
+ mcpUrl: registration.mcpUrl,
487
+ updatedAt: registration.updatedAt,
488
+ instanceName: registration.instanceName,
489
+ instanceVersion: registration.instanceVersion,
490
+ toolCount: registration.toolCount,
491
+ boundVia: registration.boundVia,
492
+ }, noticeFromQuery(), merchantId));
493
+ return;
494
+ }
495
+ if (req.method === "POST" && url.pathname === "/instance/pair") {
496
+ let form;
497
+ try {
498
+ form = await readForm(req);
499
+ }
500
+ catch {
501
+ res.writeHead(303, {
502
+ location: `/instance?error=${encodeURIComponent("表单解析失败,请重试")}`,
503
+ ...NO_STORE_HEADERS,
504
+ });
505
+ res.end();
506
+ return;
507
+ }
508
+ const mcpUrl = (form.mcp_url ?? "").trim();
509
+ const code = form.code ?? "";
510
+ try {
511
+ const bound = await bindInstanceViaPairing({ registrations: binding.registrations, credentials: binding.credentials }, { merchantId, mcpUrl, code }, {
512
+ ...(binding.redeem !== undefined ? { redeem: binding.redeem } : {}),
513
+ ...(binding.probe !== undefined ? { probe: binding.probe } : {}),
514
+ });
515
+ process.stdout.write(`[gateway entry] instance paired via code: merchant=${merchantId} owner=${bound.ownerId} url=${bound.mcpUrl}\n`);
516
+ }
517
+ catch (err) {
518
+ const message = err instanceof Error ? err.message : String(err);
519
+ res.writeHead(303, {
520
+ location: `/instance?error=${encodeURIComponent(message)}`,
521
+ ...NO_STORE_HEADERS,
522
+ });
523
+ res.end();
524
+ return;
525
+ }
526
+ res.writeHead(303, { location: "/instance?status=paired", ...NO_STORE_HEADERS });
527
+ res.end();
528
+ return;
529
+ }
530
+ if (req.method === "POST" &&
531
+ (url.pathname === "/instance/bind" || url.pathname === "/instance/unbind")) {
532
+ let form;
533
+ try {
534
+ form = await readForm(req);
535
+ }
536
+ catch {
537
+ res.writeHead(303, {
538
+ location: `/instance?error=${encodeURIComponent("表单解析失败,请重试")}`,
539
+ ...NO_STORE_HEADERS,
540
+ });
541
+ res.end();
542
+ return;
543
+ }
544
+ if (url.pathname === "/instance/unbind") {
545
+ unbindInstance({ registrations: binding.registrations, credentials: binding.credentials }, merchantId);
546
+ res.writeHead(303, { location: "/instance?status=unbound", ...NO_STORE_HEADERS });
547
+ res.end();
548
+ return;
549
+ }
550
+ const mcpUrl = (form.mcp_url ?? "").trim();
551
+ const token = form.token ?? "";
552
+ try {
553
+ // 探活成功才落库;merchantId 取自会话,表单里的任何 id 都被忽略。
554
+ await bindInstance({ registrations: binding.registrations, credentials: binding.credentials }, { merchantId, mcpUrl, token }, binding.probe !== undefined ? { probe: binding.probe } : {});
555
+ }
556
+ catch (err) {
557
+ // 失败不落任何状态;错误信息只含策略/网络结论,不含令牌。
558
+ const message = err instanceof Error ? err.message : String(err);
559
+ res.writeHead(303, {
560
+ location: `/instance?error=${encodeURIComponent(message)}`,
561
+ ...NO_STORE_HEADERS,
562
+ });
563
+ res.end();
564
+ return;
565
+ }
566
+ res.writeHead(303, { location: "/instance?status=bound", ...NO_STORE_HEADERS });
567
+ res.end();
568
+ return;
569
+ }
570
+ writeJson(res, 405, { error: "method_not_allowed" });
571
+ };
572
+ const handleMcp = async (req, res) => {
573
+ const oauth = options.oauth;
574
+ let scopes;
575
+ let authorization;
576
+ if (options.auth !== undefined) {
577
+ const verdict = options.auth.verify({
578
+ ...(typeof req.headers.authorization === "string"
579
+ ? { authorizationHeader: req.headers.authorization }
580
+ : {}),
581
+ });
582
+ if (!verdict.ok) {
583
+ writeJson(res, 401, { error: "unauthorized", message: "认证失败:需要有效的 Bearer token" }, {
584
+ "www-authenticate": `Bearer resource_metadata="${oauth.resourceMetadataUrl}"`,
585
+ });
586
+ return;
587
+ }
588
+ scopes = verdict.authorization?.scopes;
589
+ authorization = verdict.authorization;
590
+ }
591
+ if (req.method !== "POST" && req.method !== "GET" && req.method !== "DELETE") {
592
+ writeJson(res, 405, { error: "method_not_allowed" }, { allow: "POST, GET, DELETE" });
593
+ return;
594
+ }
595
+ // 每请求按已验证主体构造工具束:merchant_id 只能来自令牌,不看入参。
596
+ const tools = options.toolsFor?.(authorization) ?? options.tools;
597
+ if (tools === undefined) {
598
+ writeJson(res, 503, { error: "no_tools", message: "入口尚未装配任何工具" });
599
+ return;
600
+ }
601
+ let body;
602
+ try {
603
+ body = req.method === "POST" ? await readBody(req) : undefined;
604
+ }
605
+ catch (err) {
606
+ // 不回显异常消息(可能含内部细节):响应只给稳定结论,细节进 stderr。
607
+ process.stderr.write(`[gateway entry] mcp request body rejected: ${err instanceof Error ? err.message : String(err)}\n`);
608
+ const tooLarge = err instanceof Error && err.message.includes("exceeds");
609
+ writeJson(res, tooLarge ? 413 : 400, {
610
+ error: "invalid_request",
611
+ message: tooLarge ? "请求体超过大小上限" : "请求体无法解析",
612
+ });
613
+ return;
614
+ }
615
+ const protocolServer = createProtocolServer(serverInfo, tools, scopes);
616
+ const transport = new StreamableHTTPServerTransport({
617
+ sessionIdGenerator: undefined,
618
+ enableJsonResponse: true,
619
+ });
620
+ transports.add(transport);
621
+ res.on("close", () => {
622
+ transports.delete(transport);
623
+ void transport.close().catch(() => undefined);
624
+ void protocolServer.close().catch(() => undefined);
625
+ });
626
+ try {
627
+ await protocolServer.connect(transport);
628
+ await transport.handleRequest(req, res, body);
629
+ }
630
+ catch (err) {
631
+ // 同上:内部异常消息(可能含上游响应内容)只进 stderr,不回显给调用方。
632
+ process.stderr.write(`[gateway entry] mcp transport error: ${err instanceof Error ? err.message : String(err)}\n`);
633
+ if (!res.headersSent) {
634
+ writeJson(res, 500, { error: "internal_error", message: "入口内部错误" });
635
+ }
636
+ else {
637
+ res.end();
638
+ }
639
+ }
640
+ };
641
+ const requestHandler = (req, res) => {
642
+ void (async () => {
643
+ const url = new URL(req.url ?? "/", publicBaseUrl);
644
+ if (req.method === "GET" && url.pathname === "/health") {
645
+ writeJson(res, 200, { ok: true, service: "kiwi-merchant-entry" });
646
+ return;
647
+ }
648
+ if (await routeOAuth(req, res, url))
649
+ return;
650
+ if (req.method === "GET" && url.pathname === "/connect") {
651
+ handleConnect(res, url);
652
+ return;
653
+ }
654
+ if (req.method === "GET" && url.pathname === "/connect/callback") {
655
+ await handleConnectCallback(res, url);
656
+ return;
657
+ }
658
+ if (url.pathname === "/instance" || url.pathname.startsWith("/instance/")) {
659
+ await handleInstancePage(req, res, url);
660
+ return;
661
+ }
662
+ if (url.pathname === mcpPath) {
663
+ await handleMcp(req, res);
664
+ return;
665
+ }
666
+ writeJson(res, 404, { error: "not_found", message: `unknown path ${url.pathname}` });
667
+ })().catch((err) => {
668
+ process.stderr.write(`[gateway entry] ${req.method} ${req.url ?? "/"} 处理异常:` +
669
+ `${err instanceof Error ? err.message : String(err)}\n`);
670
+ if (!res.headersSent)
671
+ writeJson(res, 500, { error: "internal_error" });
672
+ else
673
+ res.end();
674
+ });
675
+ };
676
+ const httpServer = options.tls === undefined
677
+ ? createServer(requestHandler)
678
+ : createSecureServer({
679
+ cert: readFileSync(options.tls.certPath),
680
+ key: readFileSync(options.tls.keyPath),
681
+ }, requestHandler);
682
+ await new Promise((resolve, reject) => {
683
+ httpServer.once("error", reject);
684
+ httpServer.listen(port, host, () => resolve());
685
+ });
686
+ const address = httpServer.address();
687
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
688
+ const scheme = options.tls === undefined ? "http" : "https";
689
+ return {
690
+ host,
691
+ port: boundPort,
692
+ path: mcpPath,
693
+ secure: options.tls !== undefined,
694
+ url: `${scheme}://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${mcpPath}`,
695
+ close: async () => {
696
+ if (closing)
697
+ return;
698
+ closing = true;
699
+ for (const transport of transports) {
700
+ await transport.close().catch(() => undefined);
701
+ }
702
+ transports.clear();
703
+ await new Promise((resolve) => {
704
+ httpServer.close(() => resolve());
705
+ httpServer.closeAllConnections();
706
+ });
707
+ },
708
+ };
709
+ }
710
+ //# sourceMappingURL=entry-server.js.map