@xiaobuyu/nodesign 0.0.1 → 0.0.2

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 (282) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +464 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1024 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +339 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,180 +1,180 @@
1
- /**
2
- * server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
3
- *
4
- * ## 为什么闸从 CDP 挪到代理
5
- *
6
- * 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
7
- * 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
8
- *
9
- * - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
10
- * - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
11
- * - **`navigator.sendBeacon`**
12
- * - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
13
- * 能发 GET 和 POST,还能循环开)
14
- *
15
- * 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
16
- * 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
17
- * HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
18
- * 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
19
- *
20
- * ## 附带白拿的两件
21
- *
22
- * 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
23
- * (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
24
- * 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
25
- * 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
26
- * CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
27
- *
28
- * CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
29
- *
30
- * ## 感知通道不挂代理
31
- *
32
- * 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
33
- * 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
34
- */
35
-
36
- import http from 'node:http';
37
- import net from 'node:net';
38
- import dns from 'node:dns/promises';
39
- import { blockReason } from './ssrf-guard.js';
40
-
41
- /** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
42
- const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
43
- const RESOLVE_TIMEOUT_MS = 4000;
44
- const CONNECT_TIMEOUT_MS = 15_000;
45
-
46
- let server = null;
47
- let port = 0;
48
- const blocked = [];
49
-
50
- function note(target, reason) {
51
- blocked.push({ target, reason, at: Date.now() });
52
- if (blocked.length > 200) blocked.shift();
53
- }
54
-
55
- /**
56
- * 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
57
- * @returns {Promise<{ip: string, family: number} | {deny: string}>}
58
- */
59
- async function resolveAllowed(host, portNum) {
60
- if (!ALLOWED_PORTS.has(portNum)) {
61
- return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
62
- }
63
- const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
64
- if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
65
- return { deny: `${bare} resolves inside the local network` };
66
- }
67
- // 字面量 IP 不劳烦 DNS
68
- if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
69
- const why = blockReason(bare);
70
- return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
71
- }
72
- let addrs;
73
- try {
74
- addrs = await Promise.race([
75
- dns.lookup(bare, { all: true, verbatim: true }),
76
- new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
77
- ]);
78
- } catch (err) {
79
- // ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
80
- // 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
81
- // 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
82
- return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
83
- }
84
- if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
85
- // **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
86
- // "挑一个能用的"等于自己开门
87
- for (const a of addrs) {
88
- const why = blockReason(a.address);
89
- if (why) return { deny: `${bare} → ${why}` };
90
- }
91
- return { ip: addrs[0].address, family: addrs[0].family };
92
- }
93
-
94
- // 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
95
- // 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
96
- const denyTail = (kind) => (kind === 'dns'
97
- ? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
98
- : '内网与本机地址是硬边界,不是可配置项。');
99
-
100
- const denyBody = (reason, kind) => {
101
- const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
102
- return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
103
- + `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
104
- };
105
-
106
- /** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
107
- export async function startBrowseProxy() {
108
- if (server) return { port, blocked };
109
-
110
- server = http.createServer();
111
-
112
- // 明文 http:绝对形式的请求行(`GET http://host/path`)
113
- server.on('request', async (req, res) => {
114
- let u;
115
- try { u = new URL(req.url); } catch {
116
- res.writeHead(400); return res.end('proxy: bad absolute-form request');
117
- }
118
- const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
119
- const verdict = await resolveAllowed(u.hostname, p);
120
- if (verdict.deny) {
121
- note(`${u.hostname}:${p}`, verdict.deny);
122
- res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
123
- return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
124
- }
125
- // 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
126
- const upstream = http.request({
127
- host: verdict.ip, port: p, method: req.method,
128
- path: u.pathname + u.search,
129
- headers: { ...req.headers, host: u.host },
130
- timeout: CONNECT_TIMEOUT_MS,
131
- setHost: false,
132
- }, (up) => {
133
- res.writeHead(up.statusCode || 502, up.headers);
134
- up.pipe(res);
135
- });
136
- upstream.on('error', (err) => {
137
- if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
138
- res.end(`proxy upstream error: ${err.message}`);
139
- });
140
- req.pipe(upstream);
141
- });
142
-
143
- // CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
144
- server.on('connect', async (req, socket, head) => {
145
- const [rawHost, rawPort] = String(req.url || '').split(':');
146
- const p = Number(rawPort || 443);
147
- const verdict = await resolveAllowed(rawHost || '', p);
148
- if (verdict.deny) {
149
- note(`${rawHost}:${p}`, verdict.deny);
150
- socket.write(denyBody(verdict.deny, verdict.kind));
151
- return socket.destroy();
152
- }
153
- const up = net.connect({ host: verdict.ip, port: p }, () => {
154
- socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
155
- if (head?.length) up.write(head);
156
- socket.pipe(up); up.pipe(socket);
157
- });
158
- up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
159
- up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
160
- socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
161
- });
162
-
163
- await new Promise((resolve, reject) => {
164
- server.once('error', reject);
165
- server.listen(0, '127.0.0.1', resolve);
166
- });
167
- port = server.address().port;
168
- server.unref?.();
169
- console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
170
- return { port, blocked };
171
- }
172
-
173
- /** 给工具的返回值用:这次调用期间拦了什么 */
174
- export function blockedSince(n) {
175
- return blocked.slice(n);
176
- }
177
- export function blockedCount() {
178
- return blocked.length;
179
- }
180
- export const _proxy = { ALLOWED_PORTS };
1
+ /**
2
+ * server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
3
+ *
4
+ * ## 为什么闸从 CDP 挪到代理
5
+ *
6
+ * 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
7
+ * 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
8
+ *
9
+ * - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
10
+ * - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
11
+ * - **`navigator.sendBeacon`**
12
+ * - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
13
+ * 能发 GET 和 POST,还能循环开)
14
+ *
15
+ * 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
16
+ * 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
17
+ * HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
18
+ * 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
19
+ *
20
+ * ## 附带白拿的两件
21
+ *
22
+ * 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
23
+ * (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
24
+ * 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
25
+ * 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
26
+ * CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
27
+ *
28
+ * CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
29
+ *
30
+ * ## 感知通道不挂代理
31
+ *
32
+ * 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
33
+ * 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
34
+ */
35
+
36
+ import http from 'node:http';
37
+ import net from 'node:net';
38
+ import dns from 'node:dns/promises';
39
+ import { blockReason } from './ssrf-guard.js';
40
+
41
+ /** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
42
+ const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
43
+ const RESOLVE_TIMEOUT_MS = 4000;
44
+ const CONNECT_TIMEOUT_MS = 15_000;
45
+
46
+ let server = null;
47
+ let port = 0;
48
+ const blocked = [];
49
+
50
+ function note(target, reason) {
51
+ blocked.push({ target, reason, at: Date.now() });
52
+ if (blocked.length > 200) blocked.shift();
53
+ }
54
+
55
+ /**
56
+ * 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
57
+ * @returns {Promise<{ip: string, family: number} | {deny: string}>}
58
+ */
59
+ async function resolveAllowed(host, portNum) {
60
+ if (!ALLOWED_PORTS.has(portNum)) {
61
+ return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
62
+ }
63
+ const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
64
+ if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
65
+ return { deny: `${bare} resolves inside the local network` };
66
+ }
67
+ // 字面量 IP 不劳烦 DNS
68
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
69
+ const why = blockReason(bare);
70
+ return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
71
+ }
72
+ let addrs;
73
+ try {
74
+ addrs = await Promise.race([
75
+ dns.lookup(bare, { all: true, verbatim: true }),
76
+ new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
77
+ ]);
78
+ } catch (err) {
79
+ // ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
80
+ // 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
81
+ // 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
82
+ return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
83
+ }
84
+ if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
85
+ // **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
86
+ // "挑一个能用的"等于自己开门
87
+ for (const a of addrs) {
88
+ const why = blockReason(a.address);
89
+ if (why) return { deny: `${bare} → ${why}` };
90
+ }
91
+ return { ip: addrs[0].address, family: addrs[0].family };
92
+ }
93
+
94
+ // 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
95
+ // 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
96
+ const denyTail = (kind) => (kind === 'dns'
97
+ ? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
98
+ : '内网与本机地址是硬边界,不是可配置项。');
99
+
100
+ const denyBody = (reason, kind) => {
101
+ const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
102
+ return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
103
+ + `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
104
+ };
105
+
106
+ /** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
107
+ export async function startBrowseProxy() {
108
+ if (server) return { port, blocked };
109
+
110
+ server = http.createServer();
111
+
112
+ // 明文 http:绝对形式的请求行(`GET http://host/path`)
113
+ server.on('request', async (req, res) => {
114
+ let u;
115
+ try { u = new URL(req.url); } catch {
116
+ res.writeHead(400); return res.end('proxy: bad absolute-form request');
117
+ }
118
+ const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
119
+ const verdict = await resolveAllowed(u.hostname, p);
120
+ if (verdict.deny) {
121
+ note(`${u.hostname}:${p}`, verdict.deny);
122
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
123
+ return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
124
+ }
125
+ // 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
126
+ const upstream = http.request({
127
+ host: verdict.ip, port: p, method: req.method,
128
+ path: u.pathname + u.search,
129
+ headers: { ...req.headers, host: u.host },
130
+ timeout: CONNECT_TIMEOUT_MS,
131
+ setHost: false,
132
+ }, (up) => {
133
+ res.writeHead(up.statusCode || 502, up.headers);
134
+ up.pipe(res);
135
+ });
136
+ upstream.on('error', (err) => {
137
+ if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
138
+ res.end(`proxy upstream error: ${err.message}`);
139
+ });
140
+ req.pipe(upstream);
141
+ });
142
+
143
+ // CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
144
+ server.on('connect', async (req, socket, head) => {
145
+ const [rawHost, rawPort] = String(req.url || '').split(':');
146
+ const p = Number(rawPort || 443);
147
+ const verdict = await resolveAllowed(rawHost || '', p);
148
+ if (verdict.deny) {
149
+ note(`${rawHost}:${p}`, verdict.deny);
150
+ socket.write(denyBody(verdict.deny, verdict.kind));
151
+ return socket.destroy();
152
+ }
153
+ const up = net.connect({ host: verdict.ip, port: p }, () => {
154
+ socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
155
+ if (head?.length) up.write(head);
156
+ socket.pipe(up); up.pipe(socket);
157
+ });
158
+ up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
159
+ up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
160
+ socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
161
+ });
162
+
163
+ await new Promise((resolve, reject) => {
164
+ server.once('error', reject);
165
+ server.listen(0, '127.0.0.1', resolve);
166
+ });
167
+ port = server.address().port;
168
+ server.unref?.();
169
+ console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
170
+ return { port, blocked };
171
+ }
172
+
173
+ /** 给工具的返回值用:这次调用期间拦了什么 */
174
+ export function blockedSince(n) {
175
+ return blocked.slice(n);
176
+ }
177
+ export function blockedCount() {
178
+ return blocked.length;
179
+ }
180
+ export const _proxy = { ALLOWED_PORTS };
@@ -1,36 +1,36 @@
1
- /**
2
- * canvas-id —— agent 侧画布 id 归一化(2026-08-14,摆位批)
3
- *
4
- * id = kind 前缀 + 工作区相对路径(board 的铁律)。agent 传进来的写法五花八门
5
- * (反斜杠 / ./ 前缀 / 裸 .html),read_board / arrange_on_board / create_on_board
6
- * 共用这一份归一。跟 pin_to_board 内联的那段同源同规则 —— 收敛计划里它也该
7
- * 迁过来(现在没动它:改稳定工具要单独一刀)。
8
- */
9
-
10
- export function normalizeCanvasId(raw) {
11
- let id = String(raw || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
12
- if (!id || id.includes('..')) return null;
13
- if (id.endsWith('agent-memory/brand/memory.md')) return 'doc:brand';
14
- if (id.endsWith('agent-memory/memory.md')) return 'doc:_root';
15
- if (!/^(deck|site|doc|text|scribble):/.test(id) && /\.html?$/i.test(id)) return `deck:${id}`;
16
- return id;
17
- }
18
-
19
- /**
20
- * 它住在哪一层(服务端近似版):显式 zone 字段优先,其次沿路径往上找第一个
21
- * 已知文件夹。已知集 = board.zones 的 key —— 比前端少了"服务端扫出来的任务
22
- * 目录"这一路,没摆过的深层目录会归到根。read_board 的输出里写明这是近似。
23
- */
24
- export function layerOf(id, entry, knownFolders) {
25
- if (entry && typeof entry.zone === 'string') return entry.zone;
26
- const s = String(id);
27
- if (s.startsWith('doc:')) return '';
28
- const c = s.indexOf(':');
29
- const p = (c > 0 && /^[a-z]+$/.test(s.slice(0, c))) ? s.slice(c + 1) : s;
30
- let d = p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
31
- while (d && !knownFolders.has(d)) {
32
- const i = d.lastIndexOf('/');
33
- d = i > 0 ? d.slice(0, i) : '';
34
- }
35
- return d;
36
- }
1
+ /**
2
+ * canvas-id —— agent 侧画布 id 归一化(2026-08-14,摆位批)
3
+ *
4
+ * id = kind 前缀 + 工作区相对路径(board 的铁律)。agent 传进来的写法五花八门
5
+ * (反斜杠 / ./ 前缀 / 裸 .html),read_board / arrange_on_board / create_on_board
6
+ * 共用这一份归一。跟 pin_to_board 内联的那段同源同规则 —— 收敛计划里它也该
7
+ * 迁过来(现在没动它:改稳定工具要单独一刀)。
8
+ */
9
+
10
+ export function normalizeCanvasId(raw) {
11
+ let id = String(raw || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
12
+ if (!id || id.includes('..')) return null;
13
+ if (id.endsWith('agent-memory/brand/memory.md')) return 'doc:brand';
14
+ if (id.endsWith('agent-memory/memory.md')) return 'doc:_root';
15
+ if (!/^(deck|site|doc|text|scribble):/.test(id) && /\.html?$/i.test(id)) return `deck:${id}`;
16
+ return id;
17
+ }
18
+
19
+ /**
20
+ * 它住在哪一层(服务端近似版):显式 zone 字段优先,其次沿路径往上找第一个
21
+ * 已知文件夹。已知集 = board.zones 的 key —— 比前端少了"服务端扫出来的任务
22
+ * 目录"这一路,没摆过的深层目录会归到根。read_board 的输出里写明这是近似。
23
+ */
24
+ export function layerOf(id, entry, knownFolders) {
25
+ if (entry && typeof entry.zone === 'string') return entry.zone;
26
+ const s = String(id);
27
+ if (s.startsWith('doc:')) return '';
28
+ const c = s.indexOf(':');
29
+ const p = (c > 0 && /^[a-z]+$/.test(s.slice(0, c))) ? s.slice(c + 1) : s;
30
+ let d = p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
31
+ while (d && !knownFolders.has(d)) {
32
+ const i = d.lastIndexOf('/');
33
+ d = i > 0 ? d.slice(0, i) : '';
34
+ }
35
+ return d;
36
+ }