synabun 2.0.0 → 2.0.5

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 (334) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/THIRD-PARTY-LICENSES.md +46 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +2 -2
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +20 -0
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/start-launcher.js +88 -1
  14. package/mcp-server/package-lock.json +2 -2
  15. package/mcp-server/package.json +1 -3
  16. package/neural-interface/i18n/en.json +1500 -153
  17. package/neural-interface/i18n/pt-BR.json +1498 -152
  18. package/neural-interface/lib/assistant-api.js +62 -6
  19. package/neural-interface/lib/assistant-brains/claude.js +4 -0
  20. package/neural-interface/lib/assistant-catalog.js +179 -27
  21. package/neural-interface/lib/assistant-config.js +31 -2
  22. package/neural-interface/lib/assistant-dispatch.js +62 -13
  23. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  24. package/neural-interface/lib/assistant-memory.js +24 -10
  25. package/neural-interface/lib/assistant-persona.js +6 -2
  26. package/neural-interface/lib/assistant-pricing.js +21 -6
  27. package/neural-interface/lib/assistant-runtime.js +181 -12
  28. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  29. package/neural-interface/lib/assistant-test-density.js +142 -0
  30. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  31. package/neural-interface/lib/assistant-user-context.js +908 -0
  32. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  33. package/neural-interface/lib/assistant-user-learn.js +404 -0
  34. package/neural-interface/lib/assistant-user-threads.js +226 -0
  35. package/neural-interface/lib/assistant-user-writer.js +181 -0
  36. package/neural-interface/lib/browser-background-tab.js +134 -0
  37. package/neural-interface/lib/browser-screenshot.js +678 -61
  38. package/neural-interface/lib/claude-agent-bridge.js +33 -6
  39. package/neural-interface/lib/claude-model-catalog.js +30 -2
  40. package/neural-interface/lib/claude-temporary.js +46 -7
  41. package/neural-interface/lib/cloudflared/install.js +533 -0
  42. package/neural-interface/lib/cloudflared/manager.js +545 -0
  43. package/neural-interface/lib/cloudflared/pins.js +115 -0
  44. package/neural-interface/lib/cloudflared/pins.json +63 -0
  45. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  46. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  47. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  48. package/neural-interface/lib/editor-completion.js +714 -0
  49. package/neural-interface/lib/editor-config.js +149 -0
  50. package/neural-interface/lib/editor-files-api.js +268 -0
  51. package/neural-interface/lib/editor-git-api.js +134 -0
  52. package/neural-interface/lib/file-explorer-api.js +1039 -0
  53. package/neural-interface/lib/file-explorer-system.js +355 -0
  54. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  55. package/neural-interface/lib/file-explorer-watch.js +239 -0
  56. package/neural-interface/lib/git-diff-api.js +103 -0
  57. package/neural-interface/lib/git-read.js +202 -0
  58. package/neural-interface/lib/greeting/api.js +89 -0
  59. package/neural-interface/lib/greeting/budgets.js +90 -0
  60. package/neural-interface/lib/greeting/engine.js +461 -0
  61. package/neural-interface/lib/greeting/facts.js +320 -0
  62. package/neural-interface/lib/greeting/sources.js +252 -0
  63. package/neural-interface/lib/greeting/writer.js +139 -0
  64. package/neural-interface/lib/http-guards.js +189 -13
  65. package/neural-interface/lib/machine-names.js +88 -0
  66. package/neural-interface/lib/memory-api.js +31 -7
  67. package/neural-interface/lib/native-loop-providers.js +10 -5
  68. package/neural-interface/lib/plugin-installer.js +4 -2
  69. package/neural-interface/lib/project-storage.js +5 -7
  70. package/neural-interface/lib/request-gate.js +589 -0
  71. package/neural-interface/lib/ui-blob-api.js +108 -0
  72. package/neural-interface/lib/ui-blob-store.js +68 -0
  73. package/neural-interface/package-lock.json +701 -2
  74. package/neural-interface/package.json +13 -2
  75. package/neural-interface/public/install-app.html +9 -6
  76. package/neural-interface/public/offline.html +3 -2
  77. package/neural-interface/public/onboarding.html +12 -1
  78. package/neural-interface/public/shared/agent-runtime-options.js +22 -6
  79. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  80. package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
  81. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  82. package/neural-interface/public/shared/assistant/asst-mascot.js +18 -58
  83. package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
  84. package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
  85. package/neural-interface/public/shared/assistant/asst-render.js +162 -19
  86. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  87. package/neural-interface/public/shared/assistant/asst-route.js +23 -329
  88. package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
  89. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  90. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  91. package/neural-interface/public/shared/assistant/asst-state.js +185 -1
  92. package/neural-interface/public/shared/assistant/asst-styles.js +158 -4
  93. package/neural-interface/public/shared/blob-store.js +111 -22
  94. package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
  95. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  96. package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
  97. package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
  98. package/neural-interface/public/shared/constants.js +6 -1
  99. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  100. package/neural-interface/public/shared/cp/cp-styles.js +15 -0
  101. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  102. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  103. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  104. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  105. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  106. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  107. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  108. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  109. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  110. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  111. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  112. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  113. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  114. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  115. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  116. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  117. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  118. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  119. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  120. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  121. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  122. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  123. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  124. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  125. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  126. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  127. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  128. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  129. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  130. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  131. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  132. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  133. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  134. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  135. package/neural-interface/public/shared/file-links.js +339 -0
  136. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  137. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  138. package/neural-interface/public/shared/html-shell.js +160 -165
  139. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  140. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  141. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
  142. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  143. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
  144. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
  145. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  146. package/neural-interface/public/shared/pwa-install-page.js +29 -1
  147. package/neural-interface/public/shared/pwa-install.css +3 -3
  148. package/neural-interface/public/shared/pwa-install.js +100 -1
  149. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  150. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  151. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  152. package/neural-interface/public/shared/settings/settings-ia.js +18 -4
  153. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  154. package/neural-interface/public/shared/start-bridge.js +238 -19
  155. package/neural-interface/public/shared/styles.css +1414 -721
  156. package/neural-interface/public/shared/synabun-mascot.js +20 -56
  157. package/neural-interface/public/shared/tunnel-view.js +167 -0
  158. package/neural-interface/public/shared/ui-claude-panel.js +157 -16
  159. package/neural-interface/public/shared/ui-detail.js +15 -0
  160. package/neural-interface/public/shared/ui-explorer.js +22 -0
  161. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  162. package/neural-interface/public/shared/ui-file-links.js +103 -0
  163. package/neural-interface/public/shared/ui-help.js +26 -0
  164. package/neural-interface/public/shared/ui-invite.js +126 -116
  165. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  166. package/neural-interface/public/shared/ui-loading.js +159 -15
  167. package/neural-interface/public/shared/ui-menubar.js +12 -30
  168. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  169. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  170. package/neural-interface/public/shared/ui-navbar.js +45 -21
  171. package/neural-interface/public/shared/ui-settings.js +455 -488
  172. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  173. package/neural-interface/public/shared/ui-sync.js +16 -0
  174. package/neural-interface/public/shared/ui-terminal.js +23 -5
  175. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  176. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  177. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  178. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  179. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  180. package/neural-interface/public/shared/worker-end.js +251 -0
  181. package/neural-interface/public/sw.js +2 -2
  182. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  183. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  184. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  185. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  186. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  187. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  188. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  189. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  190. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  191. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  192. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  193. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  194. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  195. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  196. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  197. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  198. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  199. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  200. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  201. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  202. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  203. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  204. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  205. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  206. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  207. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  208. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  209. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  210. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  211. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  212. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  213. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  214. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  215. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  216. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  217. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  218. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  219. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  220. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  221. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  222. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  223. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  224. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  225. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  226. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  227. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  228. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  229. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  230. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  231. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  232. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  233. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  234. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  235. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  236. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  237. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  238. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  239. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  240. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  241. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  242. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  243. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  244. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  245. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  246. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  247. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  248. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  249. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  250. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  251. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  252. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  253. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  254. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  255. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  256. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  257. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  258. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  259. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  260. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  261. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  262. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  263. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  264. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  265. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  266. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  267. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  268. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  269. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  270. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  271. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  272. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  273. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  274. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  275. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  276. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  277. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  278. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  279. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  280. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  281. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  282. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  283. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  284. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  285. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  286. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  287. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  288. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  289. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  290. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  291. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  292. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  293. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  294. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  295. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  296. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  297. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  298. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  299. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  300. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  301. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  302. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  303. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  304. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  305. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  306. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  307. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  308. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  309. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  310. package/neural-interface/scripts/build-vendor.mjs +78 -2
  311. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  312. package/neural-interface/server.js +873 -1028
  313. package/neural-interface/templates/rulesets/README.md +1 -1
  314. package/package.json +2 -8
  315. package/setup.js +44 -7
  316. package/benchmarks/browser-live-results.json +0 -79
  317. package/benchmarks/browser-v2-results.json +0 -258
  318. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  319. package/neural-interface/public/category-logos/claude.svg +0 -1
  320. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  321. package/neural-interface/public/category-logos/openai.svg +0 -1
  322. package/neural-interface/public/category-logos/synabun.png +0 -0
  323. package/neural-interface/public/claude.svg +0 -1
  324. package/neural-interface/public/gallery.svg +0 -6
  325. package/neural-interface/public/leonardoai.svg +0 -1
  326. package/neural-interface/public/mcp.svg +0 -4
  327. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  328. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  329. package/neural-interface/public/sessionicon.svg +0 -8
  330. package/neural-interface/public/toolbar-album.svg +0 -8
  331. package/neural-interface/public/toolbar-question.svg +0 -6
  332. package/neural-interface/public/toolbar-sessions.svg +0 -6
  333. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  334. package/skills/synabun/synabunicon.png +0 -0
@@ -5,6 +5,7 @@ import {
5
5
  } from 'fs';
6
6
  import { join, resolve, dirname } from 'path';
7
7
  import os from 'node:os';
8
+ import { runGitReadSync } from './git-read.js';
8
9
 
9
10
  const PLUGINS_ROOT = () => join(os.homedir(), '.claude', 'plugins');
10
11
  const INSTALLED_PLUGINS_FILE = () => join(PLUGINS_ROOT(), 'installed_plugins.json');
@@ -48,9 +49,10 @@ function claudeNotFoundErr() {
48
49
  return { ok: false, error: 'Claude Code CLI not found on PATH. Install `claude` to manage plugins.' };
49
50
  }
50
51
 
52
+ // (git is asked as lib/git-read.js runs a read: the folder comes from a request, or from Claude Code's registry)
51
53
  function gitHeadSha(repoPath) {
52
54
  if (!repoPath || !existsSync(repoPath)) return 'unknown';
53
- const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repoPath, encoding: 'utf-8' });
55
+ const r = runGitReadSync(repoPath, ['rev-parse', 'HEAD'], { encoding: 'utf-8' });
54
56
  const out = (r.stdout || '').trim();
55
57
  return out || 'unknown';
56
58
  }
@@ -252,7 +254,7 @@ export function repairSynabunPlugins() {
252
254
  if (!meta.ok) { failed.push({ marketplaceName: name, error: meta.error }); continue; }
253
255
  let githubUrl = null;
254
256
  try {
255
- const r = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: path, encoding: 'utf-8' });
257
+ const r = runGitReadSync(path, ['remote', 'get-url', 'origin'], { encoding: 'utf-8' });
256
258
  const u = (r.stdout || '').trim();
257
259
  if (/^https?:\/\//.test(u)) githubUrl = u.replace(/\.git$/, '');
258
260
  } catch {}
@@ -18,8 +18,8 @@ import {
18
18
  resolve,
19
19
  sep,
20
20
  } from 'node:path';
21
- import { spawnSync } from 'node:child_process';
22
21
  import { isMainThread, parentPort, Worker, workerData } from 'node:worker_threads';
22
+ import { runGitReadSync } from './git-read.js';
23
23
 
24
24
  export const PROJECT_STORAGE_SAFE_CONFIRMATION = 'CLEAR SELECTED CACHES';
25
25
  export const PROJECT_STORAGE_DEPENDENCY_CONFIRMATION = 'DELETE DEPENDENCIES';
@@ -216,11 +216,11 @@ function measurePath(rootPath, { entryLimit = MEASURE_ENTRY_LIMIT } = {}) {
216
216
  return { bytes, fileCount, incomplete, containsRepository };
217
217
  }
218
218
 
219
+ // Both questions are reads, run as lib/git-read.js runs one: a registered project is a folder like any other, and
220
+ // listing its files starts nothing its own git configuration names (`git ls-files` asks the file system monitor).
219
221
  function gitInfoForProject(projectRoot) {
220
222
  const canonicalProjectRoot = canonicalExistingPath(projectRoot);
221
- const probe = spawnSync('git', ['-C', projectRoot, 'rev-parse', '--show-toplevel'], {
222
- encoding: 'utf8', timeout: 5_000, windowsHide: true,
223
- });
223
+ const probe = runGitReadSync(projectRoot, ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 5_000 });
224
224
  if (probe.status !== 0) {
225
225
  let current = canonicalProjectRoot;
226
226
  while (true) {
@@ -237,9 +237,7 @@ function gitInfoForProject(projectRoot) {
237
237
  if (!pathIsWithin(gitRoot, canonicalProjectRoot, { allowEqual: true })) {
238
238
  return { isRepository: false, verified: false, root: gitRoot, tracked: [] };
239
239
  }
240
- const listed = spawnSync('git', ['-C', gitRoot, 'ls-files', '-z'], {
241
- encoding: 'buffer', timeout: 20_000, maxBuffer: 64 * 1024 * 1024, windowsHide: true,
242
- });
240
+ const listed = runGitReadSync(gitRoot, ['ls-files', '-z'], { encoding: 'buffer', timeout: 20_000, maxBuffer: 64 * 1024 * 1024 });
243
241
  if (listed.status !== 0 || listed.error) {
244
242
  return { isRepository: true, verified: false, root: gitRoot, tracked: [] };
245
243
  }
@@ -0,0 +1,589 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — who may ask: the checks in front of every route
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Four middlewares that server.js mounts before any route (createRequestGates):
6
+ //
7
+ // host A request for the owner's data, or one that acts for them, must
8
+ // name this server: 403 { ok: false, code: 'BAD_HOST' } otherwise.
9
+ // A web page whose own name was re-pointed at this machine (DNS
10
+ // rebinding) is same-origin with the server and sends that name as
11
+ // its Host. The rule is lib/http-guards.js isAllowedHost: loopback,
12
+ // an IP address, this computer's names, the tunnel and the invite
13
+ // proxy in use, and what SYNABUN_ALLOWED_HOSTS adds. It covers
14
+ // everything under HOST_CHECKED_PREFIXES and the images the owner
15
+ // uploaded for their categories; the app's own static files stay
16
+ // open.
17
+ // remote A request that comes in from outside is nobody's until it shows
18
+ // an invite session: 403 { error: 'Forbidden' } for everything but
19
+ // the MCP endpoint and the invite sign-in. From outside is what
20
+ // Cloudflare's tunnel marks (cf-connecting-ip) and what arrives
21
+ // under a public name, the tunnel's or the invite proxy's,
22
+ // whatever headers it carries (isRemote). With a session it is a
23
+ // guest; it is never the owner. This computer's own names and
24
+ // addresses are not public names, whatever the invite address is
25
+ // set to.
26
+ // adminOnly An invite guest never reaches an admin-only route.
27
+ // features An invite guest changes nothing of a feature the owner has not
28
+ // opened to guests, and without the terminal reads neither of
29
+ // its lists of a folder's files and branches.
30
+ //
31
+ // And one for the plain /mcp mount (mcpWithoutKey): what comes in from outside
32
+ // needs the key (/mcp/<key>), and is refused while none is set. So does what
33
+ // comes from another machine on the network (fromAnotherMachine: the socket's
34
+ // peer is neither loopback nor one of this computer's own addresses), which
35
+ // server.js also asks before the tunnel is started or stopped. Those two only:
36
+ // what else a machine on the network may do is the owner's decision.
37
+ //
38
+ // A WebSocket upgrade passes no middleware: server.js asks ownHost(req) for the
39
+ // Host rule (every upgrade is under it, whatever its path) and isRemote(req)
40
+ // for where it comes from, before anything else looks at the request. A name
41
+ // that is not known while this computer's names are being read waits for
42
+ // them, as a route's request does (ownHostAfterNames).
43
+ //
44
+ // The two guest checks decide on the path as the router will match it, not on
45
+ // the path as it was written. Express matches a route whatever the case of its
46
+ // letters, takes one more slash after the path a router is mounted at, and
47
+ // decodes the parameters of a route (a handler may decode one again). So
48
+ // `/API/system/backup`, `/api//hook-recall` and `/api/ui-blobs/editor%2Dbackups`
49
+ // reach routes that a comparison of the raw path with a list of prefixes does
50
+ // not see. routedForms() gives every form a path can be read as; a prefix that
51
+ // matches any of them decides. A path that has no safe reading (a "." or ".."
52
+ // segment, a control character, more layers of escapes than anybody's own page
53
+ // writes) is refused outright: for a guest, and for a Host that is not ours.
54
+ //
55
+ // The owner's own requests are not read this way at all: with a Host of ours
56
+ // and no invite session, none of this looks at the path.
57
+
58
+ import { timingSafeEqual } from 'node:crypto';
59
+ import { readFileSync } from 'node:fs';
60
+ import { isIP } from 'node:net';
61
+ import { parse as parseDotenv } from 'dotenv';
62
+ import { hostNameOf, isAllowedHost, isLoopbackAddress, sendGuardError, thisMachineAddresses } from './http-guards.js';
63
+ import { machineNames as thisMachine } from './machine-names.js';
64
+
65
+ /** Routes an invite guest never reaches: the path, in any spelling, starts with one of these. */
66
+ export const ADMIN_ONLY_PREFIXES = Object.freeze([
67
+ '/api/settings', '/api/display-settings',
68
+ '/api/connections', '/api/setup', '/api/system',
69
+ '/api/invite/key', '/api/invite/sessions', '/api/invite/proxy', '/api/invite/permissions',
70
+ '/api/claude-code', '/api/mcp-key', '/api/mcp/runtime-profile',
71
+ // MCP servers and plugins as the owner set them up: their credentials (GET /api/mcp/env/<name>?reveal=1
72
+ // answers with them), the registry of the commands that are started, installs, what is written into the
73
+ // CLIs' own configuration, and the tool profile. Everything under /api/mcp, the two entries above included.
74
+ '/api/mcp',
75
+ '/api/plugins',
76
+ // Installing a skill into one of the owner's runtimes, or removing it (Settings; its older address is under
77
+ // /api/claude-code above). The "Skills Studio" permission opens /api/skills-studio, not this.
78
+ '/api/skills/install',
79
+ '/api/tunnel/start', '/api/tunnel/stop',
80
+ '/api/bridges', '/api/keybinds',
81
+ '/api/cli',
82
+ // The code editor: reading, writing and creating files, their versions, and its preferences.
83
+ '/api/file-content',
84
+ '/api/file-mkdir',
85
+ '/api/file-stat',
86
+ '/api/file-head',
87
+ '/api/editor',
88
+ // The editor's copies of unsaved buffers hold file content: their place in the UI blob store is the owner's.
89
+ '/api/ui-blobs/editor-backups',
90
+ // The File Explorer: its own routes (lib/file-explorer-api.js), the older listing of a project's files it
91
+ // falls back to, and the folder picker of "Add project", which opens a dialog on the owner's machine.
92
+ '/api/explorer',
93
+ '/api/project-files',
94
+ '/api/browse-folder',
95
+ '/api/codex/plans',
96
+ '/api/sidepanel',
97
+ '/api/sidepanel-runs',
98
+ '/api/loop',
99
+ '/api/schedules',
100
+ '/api/schedule-groups',
101
+ '/api/quick-timer',
102
+ '/api/diagnostics',
103
+ '/api/desktop',
104
+ // WhatsApp Link: linking, the owner and the permission level (its router
105
+ // refuses guests and remote callers on its own too: lib/http-guards.js).
106
+ '/api/whatsapp',
107
+ // TypeSafe key/config/backfill, and the hook routes: /hook-recall returns
108
+ // memory content and /hook-judge spends the host's judgment budget.
109
+ '/api/typesafe',
110
+ '/api/hook-',
111
+ '/api/map/rebuild',
112
+ ]);
113
+
114
+ /** A guest's request that is not a GET, under one of these, needs the owner to have opened that feature to guests. */
115
+ export const FEATURE_PERMISSION_MAP = Object.freeze([
116
+ { prefix: '/api/terminal', perm: 'terminal' },
117
+ { prefix: '/api/whiteboard', perm: 'whiteboard' },
118
+ { prefix: '/api/memory', perm: 'memories' },
119
+ { prefix: '/api/categories', perm: 'memories' },
120
+ { prefix: '/api/trash', perm: 'memories' },
121
+ { prefix: '/api/skills-studio', perm: 'skills' },
122
+ { prefix: '/api/browser', perm: 'browser' },
123
+ { prefix: '/api/cards', perm: 'cards' },
124
+ { prefix: '/api/style-guide', perm: 'styleGuide' },
125
+ ].map(Object.freeze));
126
+
127
+ /**
128
+ * The GETs a guest does not get for the asking. The terminal's lists of a folder's files and of its branches take
129
+ * any folder of the disk from the request, so they are the terminal's: a guest reads them with the permission
130
+ * that gives a guest the terminal, and is refused without it. A whole segment each.
131
+ */
132
+ export const FEATURE_READ_PATHS = Object.freeze([
133
+ { path: '/api/terminal/files', perm: 'terminal' },
134
+ { path: '/api/terminal/branches', perm: 'terminal' },
135
+ ].map(Object.freeze));
136
+
137
+ /**
138
+ * What the Host rule covers: every route that returns the owner's data or acts for them. Everything under /api;
139
+ * the MCP endpoint (memory, the browser, every tool); the invite sign-in (the way in for a guest); the offline
140
+ * page as the server renders it (it carries the install path and the launcher's state); the owner's uploaded file
141
+ * icons. A whole segment each: /api and /api/…, never /apix.
142
+ */
143
+ export const HOST_CHECKED_PREFIXES = Object.freeze(['/api', '/mcp', '/invite/auth', '/offline.html', '/custom-icons']);
144
+
145
+ /**
146
+ * The Host rule also covers the images the owner uploaded for their categories. They are written beside the
147
+ * app's own files (server.js: public/logo-<category>.<png|jpg|webp|gif>) and served with them, so they are known
148
+ * by their name: a file of that family at the root. The app's own files of the family (BUNDLED_LOGO_FILES) stay
149
+ * open, unless a category has taken one of those names for its upload (`claimed`).
150
+ */
151
+ export const UPLOADED_IMAGE_PATH = /^\/logo-[^/]+\.(?:png|jpe?g|webp|gif)$/;
152
+ export const BUNDLED_LOGO_FILES = Object.freeze(['/logo-synabun.png']);
153
+
154
+ /**
155
+ * What a request from outside reaches without an invite session: the MCP endpoint (it has its key) and the invite
156
+ * sign-in. As the tunnel block of server.js always read it: the path as written starts with one of these.
157
+ */
158
+ export const REMOTE_OPEN_PREFIXES = Object.freeze(['/mcp', '/invite']);
159
+
160
+ /**
161
+ * Headers that speak for an agent of this computer (the Assistant's brain, a computer-use grant). From outside
162
+ * they count for nothing: they are taken off a remote request before anything reads them.
163
+ */
164
+ export const REMOTE_STRIPPED_HEADERS = Object.freeze(['x-synabun-role', 'x-synabun-desktop-grant']);
165
+
166
+ /** The environment variable that adds names this server answers to (docs/api-reference.md → Who may call the server). */
167
+ export const ALLOWED_HOSTS_VARIABLE = 'SYNABUN_ALLOWED_HOSTS';
168
+
169
+ const MAX_DECODE_ROUNDS = 3;
170
+ const ESCAPE_RUNS = /(?:%[0-9a-f]{2})+/gi;
171
+ const ASCII_ESCAPE = /%[0-7][0-9a-f]/gi;
172
+ const CONTROL = /[\u0000-\u001f\u007f]/;
173
+ const DOTS_ONLY = /^[. ]+$/;
174
+ const MAX_REFUSED_NAMES = 100;
175
+
176
+ /** One layer of percent-escapes taken off. A run that is not UTF-8 keeps its bytes above 0x7F as they are written. */
177
+ function decodeOnce(path) {
178
+ return path.replace(ESCAPE_RUNS, (run) => {
179
+ try { return decodeURIComponent(run); }
180
+ catch { return run.replace(ASCII_ESCAPE, (one) => String.fromCharCode(parseInt(one.slice(1), 16))); }
181
+ });
182
+ }
183
+
184
+ /**
185
+ * One reading of a path, in the form the prefixes are written in: lower-case, "\" as "/", a segment without its
186
+ * `;parameters` and without dots or spaces at its end, one slash between segments. null when a segment is "." or
187
+ * ".." (or dots and spaces only): the router does not resolve those, and nothing that sends them is a page of ours.
188
+ */
189
+ function canonical(path) {
190
+ const segments = [];
191
+ for (const written of path.toLowerCase().replace(/\\/g, '/').split('/')) {
192
+ const semicolon = written.indexOf(';');
193
+ const segment = semicolon === -1 ? written : written.slice(0, semicolon);
194
+ if (DOTS_ONLY.test(segment)) return null;
195
+ const trimmed = segment.replace(/[. ]+$/, '');
196
+ if (trimmed) segments.push(trimmed);
197
+ }
198
+ return `/${segments.join('/')}`;
199
+ }
200
+
201
+ /**
202
+ * Every form a request path can be read as by the router or by a handler behind it: as written, and with one, two
203
+ * and three layers of percent-escapes taken off, each made canonical. A check that compares prefixes takes a match
204
+ * in any of them.
205
+ * @param {string} path the path as the router takes it (req.path: no query, no fragment)
206
+ * @returns {string[] | null} null: the path has no safe reading
207
+ */
208
+ export function routedForms(path) {
209
+ if (typeof path !== 'string' || path[0] !== '/') return null;
210
+ const forms = [];
211
+ let current = path;
212
+ for (let round = 0; ; round++) {
213
+ if (CONTROL.test(current)) return null;
214
+ const form = canonical(current);
215
+ if (form === null) return null;
216
+ if (!forms.includes(form)) forms.push(form);
217
+ const next = decodeOnce(current);
218
+ if (next === current) return forms;
219
+ if (round === MAX_DECODE_ROUNDS) return null;
220
+ current = next;
221
+ }
222
+ }
223
+
224
+ const startsWithSegment = (form, prefix) => form === prefix || form.startsWith(`${prefix}/`);
225
+
226
+ /** Is this path, in any of its forms, one the Host rule covers? Also true for a path without a safe reading. */
227
+ export function isHostCheckedPath(path) {
228
+ const forms = routedForms(path);
229
+ return !forms || forms.some((form) => HOST_CHECKED_PREFIXES.some((prefix) => startsWithSegment(form, prefix)));
230
+ }
231
+
232
+ /**
233
+ * Is this path, in any of its forms, an image the owner uploaded for a category (UPLOADED_IMAGE_PATH)? One of the
234
+ * app's own files of that family is not, unless `claimed` says a category's upload now has that name.
235
+ * @param {string} path the path as the router takes it
236
+ * @param {(form: string) => boolean} [claimed] is this path (lower-case, as a category records its image) an upload?
237
+ */
238
+ export function isUploadedImagePath(path, claimed = () => false) {
239
+ const forms = routedForms(path);
240
+ if (!forms) return false; // no safe reading: isHostCheckedPath has that case
241
+ return forms.some((form) => {
242
+ if (!UPLOADED_IMAGE_PATH.test(form)) return false;
243
+ if (!BUNDLED_LOGO_FILES.includes(form)) return true;
244
+ try { return !!claimed(form); } catch { return true; }
245
+ });
246
+ }
247
+
248
+ /**
249
+ * The names a value of SYNABUN_ALLOWED_HOSTS adds: separated by commas or blanks, each a host name, host:port or
250
+ * an http(s) URL (isAllowedHost reads the host name of each; anything else counts for nothing). A value counts
251
+ * up to its first "#": no host is written with one, so what follows it is a comment, and a comment adds no name.
252
+ * @param {...(string | undefined | null)} values the variable as the environment has it, as the .env file has it
253
+ */
254
+ export function extraHostNames(...values) {
255
+ const names = [];
256
+ for (const value of values) {
257
+ if (typeof value !== 'string') continue;
258
+ for (const each of value.split('#', 1)[0].replace(/^\s*(['"])(.*)\1\s*$/, '$2').split(/[\s,]+/)) {
259
+ if (each && !names.includes(each)) names.push(each);
260
+ }
261
+ }
262
+ return names;
263
+ }
264
+
265
+ /**
266
+ * SYNABUN_ALLOWED_HOSTS as the data home's .env has it now, read the way the server read that file when it
267
+ * started: with dotenv's own parser. What counts is what dotenv gives the line (`export NAME=…`, blanks around
268
+ * the "=", a quoted value, a comment after it), so a line means the same before and after a restart. Read when a
269
+ * request asks: a name added to the file is answered at the next request, one taken out is refused at the next.
270
+ * @param {string} path the .env file
271
+ * @param {string} [variable]
272
+ * @returns {string | undefined} undefined: the file has no such line, or there is no file
273
+ */
274
+ export function savedValueOf(path, variable = ALLOWED_HOSTS_VARIABLE) {
275
+ try {
276
+ const value = parseDotenv(readFileSync(path, 'utf8'))[variable];
277
+ return typeof value === 'string' ? value : undefined;
278
+ } catch {
279
+ return undefined;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * SYNABUN_ALLOWED_HOSTS as the process was started with it, kept apart from what the data home's .env says.
285
+ * Loading that file into the environment copies the file's value into it, and there it stays when the line is
286
+ * taken out of the file again: a name could be added without a restart and never removed. So the value the
287
+ * environment has by itself is noted before the file is loaded (`value`), and put back afterwards (`restore`);
288
+ * the file's own value is read from the file when a request asks. Nothing the server starts inherits the file's.
289
+ * @param {object} [env] process.env
290
+ * @param {string} [variable]
291
+ * @returns {{ value: string | undefined, restore: () => void }}
292
+ */
293
+ export function startedValueOf(env = process.env, variable = ALLOWED_HOSTS_VARIABLE) {
294
+ const had = Object.prototype.hasOwnProperty.call(env, variable);
295
+ const value = had ? env[variable] : undefined;
296
+ return {
297
+ value,
298
+ restore() {
299
+ if (had) env[variable] = value;
300
+ else delete env[variable];
301
+ },
302
+ };
303
+ }
304
+
305
+ /** The host names and addresses of a list of http(s) URLs, host names or host:port entries, as hostNameOf writes them. */
306
+ export function hostsOf(list) {
307
+ let entries;
308
+ try { entries = typeof list === 'function' ? list() : list; } catch { return []; }
309
+ const names = [];
310
+ for (const entry of Array.isArray(entries) ? entries : []) {
311
+ if (!entry) continue;
312
+ let host = String(entry);
313
+ if (host.includes('://')) {
314
+ try {
315
+ const url = new URL(host);
316
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') continue;
317
+ host = url.host;
318
+ } catch { continue; }
319
+ }
320
+ const name = hostNameOf(host);
321
+ if (name) names.push(name);
322
+ }
323
+ return names;
324
+ }
325
+
326
+ /**
327
+ * A socket's peer address as this computer's own addresses are written (thisMachineAddresses): IPv4 without the
328
+ * "::ffff:" of a dual-stack socket, IPv6 in brackets without its zone. null when there is none.
329
+ */
330
+ function peerName(address) {
331
+ if (typeof address !== 'string' || !address) return null;
332
+ const bare = address.replace(/%.*$/, '');
333
+ const v4 = /^::ffff:/i.test(bare) && isIP(bare.slice(7)) === 4 ? bare.slice(7) : bare;
334
+ if (isIP(v4) === 4) return v4;
335
+ return isIP(v4) === 6 ? hostNameOf(`[${v4}]`) : null;
336
+ }
337
+
338
+ function sameText(a, b) {
339
+ const left = Buffer.from(String(a));
340
+ const right = Buffer.from(String(b));
341
+ return left.length === right.length && timingSafeEqual(left, right);
342
+ }
343
+
344
+ /** `/mcp/<key>` with the key that is set: the caller's proof is in the path, whatever name it used to get here. */
345
+ function carriesMcpKey(path, key) {
346
+ if (!key || typeof key !== 'string' || typeof path !== 'string' || !path.startsWith('/mcp/')) return false;
347
+ const end = path.indexOf('/', 5);
348
+ return sameText(end === -1 ? path.slice(5) : path.slice(5, end), key);
349
+ }
350
+
351
+ /**
352
+ * The checks in front of the routes.
353
+ * @param {object} deps
354
+ * @param {(req: object) => boolean} deps.isGuestRequest an invite guest's request?
355
+ * @param {() => object} [deps.permissions] what the owner opened to guests ({ terminal, whiteboard, memories, … })
356
+ * @param {string[] | (() => string[])} [deps.allowedHosts] the public names of this server: the tunnel's and the
357
+ * invite proxy's URLs, and the names of SYNABUN_ALLOWED_HOSTS (asked only when no local rule matched)
358
+ * @param {() => (string | null)} [deps.mcpKey] the key of /mcp/<key>, when one is set
359
+ * @param {string[] | (() => string[])} [deps.publicHosts] the names requests from outside arrive under: the
360
+ * tunnel's URL and the invite proxy's when one is in use. Never the names of SYNABUN_ALLOWED_HOSTS, which are
361
+ * the owner's own for this computer
362
+ * @param {(req: object) => boolean} [deps.hasInviteSession] does this request carry an invite session that is
363
+ * still good? (nobody has one when this is left out)
364
+ * @param {(path: string) => boolean} [deps.uploadedImages] is this root file (lower-case: "/logo-<name>.<ext>")
365
+ * the image a category recorded as its upload? Asked only for the app's own files of that family
366
+ * @param {() => string[]} [deps.localAddresses] this computer's own addresses, as a Host header names them
367
+ * (lib/http-guards.js thisMachineAddresses when it is left out)
368
+ * @param {{ names: () => string[], refresh: () => Promise<any>, stale: () => boolean }} [deps.machine]
369
+ * lib/machine-names.js; a name that is not known is checked again after the system was asked anew
370
+ * @param {() => string} [deps.settingsHint] where SYNABUN_ALLOWED_HOSTS is set, for the log line (the .env's path)
371
+ * @param {(line: string) => void} [deps.log]
372
+ * @returns {{ host: Function, remote: Function, adminOnly: Function, features: Function, mcpWithoutKey: Function,
373
+ * ownHost: (req: object) => boolean, ownHostAfterNames: (req: object, socket: object, again: () => void) => boolean,
374
+ * isRemote: (req: object) => boolean, fromAnotherMachine: (req: object) => boolean, refusedHosts: () => string[] }}
375
+ */
376
+ export function createRequestGates({
377
+ isGuestRequest, permissions = () => ({}), allowedHosts = [], mcpKey = () => null, machine = thisMachine,
378
+ settingsHint = () => '', log = (line) => console.warn(line),
379
+ publicHosts = [], hasInviteSession = () => false, uploadedImages = () => false, localAddresses = thisMachineAddresses,
380
+ } = {}) {
381
+ if (typeof isGuestRequest !== 'function') throw new Error('createRequestGates requires isGuestRequest');
382
+ const refused = new Set(); // names already written to the log
383
+ const forms = new WeakMap(); // req → routedForms(req.path), read once for the two guest checks
384
+ const formsOf = (req) => {
385
+ if (!forms.has(req)) forms.set(req, routedForms(req.path));
386
+ return forms.get(req);
387
+ };
388
+ const ours = (host) => isAllowedHost(host, { allowedHosts, machineName: machine.names });
389
+
390
+ /** One line per name: what was refused, and what to do when the name is the owner's. */
391
+ function note(req) {
392
+ const written = req.headers?.host;
393
+ const name = hostNameOf(written);
394
+ const label = name ?? (typeof written === 'string' ? JSON.stringify(written.slice(0, 80)) : 'no Host header');
395
+ if (refused.has(label)) return;
396
+ if (refused.size >= MAX_REFUSED_NAMES) {
397
+ if (!refused.has('…')) { refused.add('…'); log('[host] More names were refused; they are no longer listed one by one.'); }
398
+ return;
399
+ }
400
+ refused.add(label);
401
+ if (!name) {
402
+ log(`[host] Refused a request whose Host header is not a name a browser sends (${label}).`);
403
+ return;
404
+ }
405
+ let where = '';
406
+ try { where = String(settingsHint() || ''); } catch { where = ''; }
407
+ log(`[host] Refused a request for "${name}": this server does not answer to that name. It answers to localhost, `
408
+ + 'an IP address, this computer\'s own names and the tunnel or invite proxy in use. If the name is this '
409
+ + `computer's too, add ${ALLOWED_HOSTS_VARIABLE}=${name}${where ? ` to ${where}` : ''} (several names: `
410
+ + 'separated by commas; it is read again without a restart). If it is not, a web page tried to reach this '
411
+ + 'server under a name of its own and was stopped.');
412
+ }
413
+
414
+ function refuseHost(req, res) {
415
+ note(req);
416
+ return sendGuardError(res, 403, 'BAD_HOST', `This server does not answer to the name in the request's Host header. Open it at localhost or an IP address, or add the name to ${ALLOWED_HOSTS_VARIABLE}.`);
417
+ }
418
+
419
+ function host(req, res, next) {
420
+ const written = req.headers?.host;
421
+ if (ours(written)) return next();
422
+ let key = null;
423
+ try { key = mcpKey(); } catch { key = null; }
424
+ if (carriesMcpKey(req.path, key)) return next();
425
+ if (!isHostCheckedPath(req.path) && !isUploadedImagePath(req.path, uploadedImages)) return next();
426
+ // The computer may have been renamed since its names were read: they are read again (once a minute at most)
427
+ // before a name is refused.
428
+ if (machine.stale()) {
429
+ machine.refresh().then(() => (ours(written) ? next() : refuseHost(req, res)), () => refuseHost(req, res));
430
+ return undefined;
431
+ }
432
+ return refuseHost(req, res);
433
+ }
434
+
435
+ const waited = new WeakSet(); // upgrades that have waited for this computer's names: each waits once
436
+ const twoHosts = (req) => { const named = req?.headersDistinct?.host; return Array.isArray(named) && named.length > 1; };
437
+ /**
438
+ * Could a reading of this computer's names still make this upgrade's Host ours? Only a name can become one
439
+ * (what is no name, and two Hosts, stay refused), only while the answers are old or on their way
440
+ * (`machine.stale()`: never where the system is not asked), and only once per upgrade.
441
+ */
442
+ function namesDue(req) {
443
+ if (!req || typeof req !== 'object' || waited.has(req) || twoHosts(req) || hostNameOf(req.headers?.host) === null) return false;
444
+ try { return machine.stale() === true; } catch { return false; }
445
+ }
446
+
447
+ /**
448
+ * The Host rule for a WebSocket upgrade: is its Host one of this server's names? Every upgrade is under the
449
+ * rule, whatever its path and whether it carries an Origin or not (no file of the app is served over one, and
450
+ * a client without an Origin passes the Origin check). It answers from what is known now, at once: a caller
451
+ * that got `false` asks ownHostAfterNames() whether the system's names, which `host` waits for, are worth
452
+ * waiting for here too. A refused name is logged once, when the refusal is the last word.
453
+ * An upgrade that names two Hosts is refused whatever they are: Node reads the first, and a proxy in front may
454
+ * have gone by the other.
455
+ */
456
+ function ownHost(req) {
457
+ if (twoHosts(req)) {
458
+ if (!refused.has('two Host headers')) { refused.add('two Host headers'); log('[host] Refused a WebSocket upgrade that names two Hosts: nothing of ours sends that.'); }
459
+ return false;
460
+ }
461
+ if (ours(req?.headers?.host)) return true;
462
+ if (!namesDue(req)) note({ headers: req?.headers || {} });
463
+ return false;
464
+ }
465
+
466
+ /**
467
+ * For an upgrade ownHost() has just said no to: the computer's names are read when the server starts and again
468
+ * when the answers are old, and until a reading has ended the Bonjour name is not known. `host` waits for the
469
+ * reading before it refuses a request; an upgrade does the same here, so a page left open under that name, or
470
+ * a client that connects directly, is not answered 403 right after a start.
471
+ * → false: nothing to wait for (see namesDue). The caller refuses now.
472
+ * → true: `again()` is called when the reading has ended, and the caller asks ownHost() once more, which then
473
+ * gives the last word. Not called when the socket went away meanwhile.
474
+ * The reading is the one everybody shares (`machine.refresh()`, asked only while `machine.stale()`): names that
475
+ * are not known cannot have the system asked more often than its cache allows. Node has let go of an upgrade's
476
+ * socket, error listener included, so one is kept on it for as long as it waits here: a connection reset in
477
+ * that time would otherwise be an uncaught exception.
478
+ */
479
+ function ownHostAfterNames(req, socket, again) {
480
+ if (typeof again !== 'function' || !namesDue(req)) return false;
481
+ waited.add(req);
482
+ const quiet = () => {};
483
+ try { socket?.on?.('error', quiet); } catch { /* not a socket */ }
484
+ const ask = () => {
485
+ try { socket?.off?.('error', quiet); } catch { /* not a socket */ }
486
+ if (!socket?.destroyed) again();
487
+ };
488
+ let read;
489
+ try { read = Promise.resolve(machine.refresh()); } catch { read = Promise.resolve(); }
490
+ read.catch(() => {}).then(ask);
491
+ return true;
492
+ }
493
+
494
+ /** Does this Host (or forwarded Host) say the request came in under a public name: the tunnel's, the invite proxy's? */
495
+ function underPublicName(written) {
496
+ const name = hostNameOf(written);
497
+ if (!name) return false;
498
+ const address = name.startsWith('[') ? name.slice(1, -1) : (isIP(name) === 4 ? name : null);
499
+ // This computer itself is nobody else's way in, whatever the invite address was set to: under loopback, one
500
+ // of its own names or one of its own addresses the owner's requests arrive, as they always did. (What is
501
+ // cheap to know is asked first: the public names are read from a file.)
502
+ if (address ? isLoopbackAddress(address) : isAllowedHost(name, { allowedHosts: [], machineName: machine.names })) return false;
503
+ if (!hostsOf(publicHosts).includes(name)) return false;
504
+ if (!address) return true;
505
+ let own = [];
506
+ try { own = localAddresses() || []; } catch { own = []; }
507
+ return !own.includes(name);
508
+ }
509
+
510
+ /**
511
+ * Did this request come in from outside? Cloudflare's tunnel says so with a header; a request under a public
512
+ * name (the Host it names, or the one a proxy says it was sent to) is from outside whatever it carries.
513
+ * Reads headers only: a WebSocket upgrade is asked the same way.
514
+ */
515
+ function isRemote(req) {
516
+ const headers = req?.headers || {};
517
+ if (headers['cf-connecting-ip'] !== undefined) return true;
518
+ if (underPublicName(headers.host)) return true;
519
+ const forwarded = headers['x-forwarded-host'];
520
+ return typeof forwarded === 'string' && forwarded.split(',').some((each) => underPublicName(each.trim()));
521
+ }
522
+
523
+ function remote(req, res, next) {
524
+ if (!isRemote(req)) return next();
525
+ for (const name of REMOTE_STRIPPED_HEADERS) delete req.headers[name];
526
+ if (REMOTE_OPEN_PREFIXES.some((prefix) => req.path.startsWith(prefix))) return next();
527
+ let session = false;
528
+ try { session = !!hasInviteSession(req); } catch { session = false; }
529
+ if (session) return next();
530
+ return res.status(403).json({ error: 'Forbidden' });
531
+ }
532
+
533
+ /**
534
+ * Did this request's connection come from another machine? Its socket's peer (the socket's own address, never a
535
+ * header) is neither a loopback address nor one of this computer's own addresses; a request without one counts
536
+ * as another machine's. Asked for two things only: the plain /mcp mount (mcpWithoutKey) and the tunnel's start
537
+ * and stop (server.js). What else a machine on the network may do is the owner's decision, not this check's.
538
+ * The tunnel, and a proxy on this computer, connect from loopback; a proxy on another machine is that machine.
539
+ */
540
+ function fromAnotherMachine(req) {
541
+ const peer = req?.socket?.remoteAddress;
542
+ if (isLoopbackAddress(peer)) return false;
543
+ const name = peerName(peer);
544
+ if (!name) return true;
545
+ let own = [];
546
+ try { own = localAddresses() || []; } catch { own = []; }
547
+ return !own.includes(name);
548
+ }
549
+
550
+ /**
551
+ * In front of the plain /mcp mount: from outside, and from another machine on the network, the endpoint is
552
+ * reached with its key (/mcp/<key>) and no other way. While no key is set there is no way in from there at all:
553
+ * the tunnel's or the invite proxy's address, or this computer's address on the network, by itself opens the
554
+ * memory, the browser and every other tool to nobody. On this computer the plain endpoint answers as it always
555
+ * did, key or no key. (The same answer with a key set and without: it does not say which.)
556
+ */
557
+ function mcpWithoutKey(req, res, next) {
558
+ if (isRemote(req) || fromAnotherMachine(req)) return res.status(403).json({ error: 'Forbidden' });
559
+ return next();
560
+ }
561
+
562
+ function adminOnly(req, res, next) {
563
+ if (!isGuestRequest(req)) return next();
564
+ const read = formsOf(req);
565
+ if (!read) return res.status(400).json({ error: 'Unsupported path' });
566
+ if (read.some((form) => ADMIN_ONLY_PREFIXES.some((prefix) => form.startsWith(prefix)))) {
567
+ return res.status(403).json({ error: 'Admin only' });
568
+ }
569
+ return next();
570
+ }
571
+
572
+ function features(req, res, next) {
573
+ if (!isGuestRequest(req)) return next();
574
+ const read = formsOf(req);
575
+ if (!read) return res.status(400).json({ error: 'Unsupported path' });
576
+ // A guest's GET is free but for the few that are a feature's own (FEATURE_READ_PATHS); anything else a guest
577
+ // sends under a feature needs that feature.
578
+ const match = req.method === 'GET'
579
+ ? FEATURE_READ_PATHS.find((entry) => read.some((form) => startsWithSegment(form, entry.path)))
580
+ : FEATURE_PERMISSION_MAP.find((entry) => read.some((form) => form.startsWith(entry.prefix)));
581
+ if (match && !permissions()?.[match.perm]) return res.status(403).json({ error: 'Feature not enabled' });
582
+ return next();
583
+ }
584
+
585
+ // The names are read now, so the first request under the Bonjour name finds them known.
586
+ try { Promise.resolve(machine.refresh()).catch(() => {}); } catch { /* a machine that cannot be asked adds no name */ }
587
+
588
+ return { host, ownHost, ownHostAfterNames, remote, adminOnly, features, mcpWithoutKey, isRemote, fromAnotherMachine, refusedHosts: () => [...refused] };
589
+ }