synabun 2.0.0 → 2.0.7

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 (507) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/NOTICE +5 -0
  3. package/README.md +2 -1
  4. package/THIRD-PARTY-LICENSES.md +59 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +153 -18
  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 +26 -3
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/child-path.js +303 -0
  14. package/lib/claude-hooks.js +2 -1
  15. package/lib/data-home-migration.js +2 -0
  16. package/lib/desktop-app.js +265 -0
  17. package/lib/external-tools.js +7 -3
  18. package/lib/log-redact.js +102 -0
  19. package/lib/packaged-runtime.js +138 -11
  20. package/lib/start-launcher.js +88 -1
  21. package/mcp-server/dist/http.d.ts +3 -1
  22. package/mcp-server/dist/http.d.ts.map +1 -1
  23. package/mcp-server/dist/http.js +15 -4
  24. package/mcp-server/dist/http.js.map +1 -1
  25. package/mcp-server/dist/index.d.ts.map +1 -1
  26. package/mcp-server/dist/index.js +9 -6
  27. package/mcp-server/dist/index.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  33. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  34. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  35. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  36. package/mcp-server/dist/services/identity.d.ts +11 -0
  37. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  38. package/mcp-server/dist/services/identity.js +24 -0
  39. package/mcp-server/dist/services/identity.js.map +1 -1
  40. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  41. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  42. package/mcp-server/dist/services/neural-interface.js +34 -2
  43. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  44. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  45. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  46. package/mcp-server/dist/services/typesafe-config.js +4 -1
  47. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  48. package/mcp-server/dist/tools/agents.d.ts +10 -7
  49. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  50. package/mcp-server/dist/tools/agents.js +34 -13
  51. package/mcp-server/dist/tools/agents.js.map +1 -1
  52. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  53. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  54. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  55. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  56. package/mcp-server/dist/tui.js +2 -1
  57. package/mcp-server/dist/tui.js.map +1 -1
  58. package/mcp-server/dist/version.d.ts +5 -0
  59. package/mcp-server/dist/version.d.ts.map +1 -0
  60. package/mcp-server/dist/version.js +21 -0
  61. package/mcp-server/dist/version.js.map +1 -0
  62. package/mcp-server/package-lock.json +2 -2
  63. package/mcp-server/package.json +1 -3
  64. package/mcp-server/run.mjs +15 -9
  65. package/mcp-server/src/http.ts +18 -3
  66. package/mcp-server/src/index.ts +9 -6
  67. package/mcp-server/src/services/budget-judgments.ts +183 -0
  68. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  69. package/mcp-server/src/services/identity.ts +27 -0
  70. package/mcp-server/src/services/neural-interface.ts +34 -2
  71. package/mcp-server/src/services/typesafe-config.ts +4 -1
  72. package/mcp-server/src/tools/agents.ts +33 -14
  73. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  74. package/mcp-server/src/tui.ts +2 -1
  75. package/mcp-server/src/version.ts +22 -0
  76. package/neural-interface/i18n/en.json +2126 -209
  77. package/neural-interface/i18n/pt-BR.json +2123 -207
  78. package/neural-interface/lib/account-usage.js +163 -0
  79. package/neural-interface/lib/assistant-api.js +316 -57
  80. package/neural-interface/lib/assistant-brains/claude.js +19 -2
  81. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  82. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  83. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  84. package/neural-interface/lib/assistant-budget-split.js +408 -0
  85. package/neural-interface/lib/assistant-budget.js +94 -95
  86. package/neural-interface/lib/assistant-catalog.js +179 -27
  87. package/neural-interface/lib/assistant-config.js +91 -14
  88. package/neural-interface/lib/assistant-dispatch.js +494 -138
  89. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  90. package/neural-interface/lib/assistant-jev.js +26 -3
  91. package/neural-interface/lib/assistant-memory.js +24 -10
  92. package/neural-interface/lib/assistant-persona.js +73 -12
  93. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  94. package/neural-interface/lib/assistant-pricing.js +21 -6
  95. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  96. package/neural-interface/lib/assistant-route-gate.js +19 -3
  97. package/neural-interface/lib/assistant-router.js +373 -42
  98. package/neural-interface/lib/assistant-runtime.js +488 -59
  99. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  100. package/neural-interface/lib/assistant-test-density.js +142 -0
  101. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  102. package/neural-interface/lib/assistant-user-context.js +908 -0
  103. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  104. package/neural-interface/lib/assistant-user-learn.js +404 -0
  105. package/neural-interface/lib/assistant-user-threads.js +226 -0
  106. package/neural-interface/lib/assistant-user-writer.js +183 -0
  107. package/neural-interface/lib/augmented-path.js +317 -53
  108. package/neural-interface/lib/browser-background-tab.js +134 -0
  109. package/neural-interface/lib/browser-screenshot.js +678 -61
  110. package/neural-interface/lib/claude-accounts.js +342 -31
  111. package/neural-interface/lib/claude-agent-bridge.js +123 -9
  112. package/neural-interface/lib/claude-executable.js +6 -1
  113. package/neural-interface/lib/claude-model-catalog.js +33 -5
  114. package/neural-interface/lib/claude-panel-auth.js +221 -0
  115. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  116. package/neural-interface/lib/claude-temporary.js +46 -7
  117. package/neural-interface/lib/cli-install-api.js +137 -0
  118. package/neural-interface/lib/cli-install.js +797 -0
  119. package/neural-interface/lib/cli-path.js +130 -0
  120. package/neural-interface/lib/cli-resolver.js +744 -0
  121. package/neural-interface/lib/cli-update-planner.js +143 -7
  122. package/neural-interface/lib/cloudflared/install.js +533 -0
  123. package/neural-interface/lib/cloudflared/manager.js +545 -0
  124. package/neural-interface/lib/cloudflared/pins.js +115 -0
  125. package/neural-interface/lib/cloudflared/pins.json +63 -0
  126. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  127. package/neural-interface/lib/codex-account-auth.js +178 -0
  128. package/neural-interface/lib/codex-accounts.js +489 -0
  129. package/neural-interface/lib/codex-capabilities.js +2 -1
  130. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  131. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  132. package/neural-interface/lib/codex-panel-auth.js +62 -0
  133. package/neural-interface/lib/codex-session-settings.js +1 -1
  134. package/neural-interface/lib/codex-settings-schema.js +120 -0
  135. package/neural-interface/lib/desktop/config.js +3 -0
  136. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  137. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  138. package/neural-interface/lib/editor-completion.js +714 -0
  139. package/neural-interface/lib/editor-config.js +149 -0
  140. package/neural-interface/lib/editor-files-api.js +268 -0
  141. package/neural-interface/lib/editor-git-api.js +134 -0
  142. package/neural-interface/lib/file-explorer-api.js +1039 -0
  143. package/neural-interface/lib/file-explorer-system.js +355 -0
  144. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  145. package/neural-interface/lib/file-explorer-watch.js +239 -0
  146. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  147. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  148. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  149. package/neural-interface/lib/git-bash/install.js +529 -0
  150. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  151. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  152. package/neural-interface/lib/git-bash/manager.js +268 -0
  153. package/neural-interface/lib/git-bash/pins.js +206 -0
  154. package/neural-interface/lib/git-bash/pins.json +34 -0
  155. package/neural-interface/lib/git-bash/resolver.js +194 -0
  156. package/neural-interface/lib/git-diff-api.js +103 -0
  157. package/neural-interface/lib/git-read.js +234 -0
  158. package/neural-interface/lib/greeting/api.js +89 -0
  159. package/neural-interface/lib/greeting/budgets.js +90 -0
  160. package/neural-interface/lib/greeting/engine.js +461 -0
  161. package/neural-interface/lib/greeting/facts.js +320 -0
  162. package/neural-interface/lib/greeting/sources.js +252 -0
  163. package/neural-interface/lib/greeting/writer.js +139 -0
  164. package/neural-interface/lib/http-guards.js +189 -13
  165. package/neural-interface/lib/install-details.js +145 -0
  166. package/neural-interface/lib/machine-names.js +88 -0
  167. package/neural-interface/lib/mcp-installer.js +14 -2
  168. package/neural-interface/lib/memory-api.js +31 -7
  169. package/neural-interface/lib/native-loop-providers.js +26 -8
  170. package/neural-interface/lib/native-loop-runtime.js +9 -4
  171. package/neural-interface/lib/os-path.js +319 -0
  172. package/neural-interface/lib/plugin-installer.js +4 -2
  173. package/neural-interface/lib/project-storage.js +5 -7
  174. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  175. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  176. package/neural-interface/lib/pty-host/core.js +2 -1
  177. package/neural-interface/lib/pty-host/manager.js +27 -2
  178. package/neural-interface/lib/remote-policy.js +20 -11
  179. package/neural-interface/lib/request-gate.js +589 -0
  180. package/neural-interface/lib/session-title-generator.js +4 -3
  181. package/neural-interface/lib/system-about.js +340 -0
  182. package/neural-interface/lib/system-activity.js +66 -0
  183. package/neural-interface/lib/terminal-env.js +11 -3
  184. package/neural-interface/lib/terminal-path.js +55 -0
  185. package/neural-interface/lib/ui-blob-api.js +108 -0
  186. package/neural-interface/lib/ui-blob-store.js +68 -0
  187. package/neural-interface/lib/virtual-socket.js +12 -1
  188. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  189. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  190. package/neural-interface/lib/whatsapp/commands.js +13 -14
  191. package/neural-interface/lib/whatsapp/config.js +134 -20
  192. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  193. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  194. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  195. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  196. package/neural-interface/lib/whatsapp/manager.js +92 -32
  197. package/neural-interface/lib/whatsapp/paths.js +11 -1
  198. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  199. package/neural-interface/lib/whatsapp/redact.js +63 -0
  200. package/neural-interface/lib/whatsapp/service.js +343 -91
  201. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  202. package/neural-interface/package-lock.json +701 -2
  203. package/neural-interface/package.json +13 -2
  204. package/neural-interface/public/index.html +2 -0
  205. package/neural-interface/public/index2d.html +2 -0
  206. package/neural-interface/public/install-app.html +9 -6
  207. package/neural-interface/public/offline.html +19 -3
  208. package/neural-interface/public/onboarding.html +107 -2
  209. package/neural-interface/public/shared/account-i18n.js +7 -0
  210. package/neural-interface/public/shared/account-menu.js +539 -0
  211. package/neural-interface/public/shared/account-usage.js +19 -0
  212. package/neural-interface/public/shared/account-view.js +335 -0
  213. package/neural-interface/public/shared/agent-runtime-options.js +50 -14
  214. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  215. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  216. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  217. package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
  218. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  219. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  220. package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
  221. package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
  222. package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
  223. package/neural-interface/public/shared/assistant/asst-render.js +197 -23
  224. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  225. package/neural-interface/public/shared/assistant/asst-route.js +95 -343
  226. package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
  227. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  228. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  229. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  230. package/neural-interface/public/shared/assistant/asst-state.js +186 -2
  231. package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
  232. package/neural-interface/public/shared/blob-store.js +111 -22
  233. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  234. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  235. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  236. package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
  237. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  238. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  239. package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
  240. package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
  241. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  242. package/neural-interface/public/shared/claude-account-view.js +3 -0
  243. package/neural-interface/public/shared/cli-install-view.js +392 -0
  244. package/neural-interface/public/shared/cli-status.js +282 -22
  245. package/neural-interface/public/shared/constants.js +6 -1
  246. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  247. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  248. package/neural-interface/public/shared/cp/cp-styles.js +61 -0
  249. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  250. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  251. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  252. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  253. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  254. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  255. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  256. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  257. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  258. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  259. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  260. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  261. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  262. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  263. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  264. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  265. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  266. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  267. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  268. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  269. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  270. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  271. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  272. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  273. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  274. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  275. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  276. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  277. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  278. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  279. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  280. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  281. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  282. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  283. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  284. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  285. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  286. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  287. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  288. package/neural-interface/public/shared/file-links.js +339 -0
  289. package/neural-interface/public/shared/git-bash-view.js +145 -0
  290. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  291. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  292. package/neural-interface/public/shared/html-shell.js +161 -165
  293. package/neural-interface/public/shared/i18n.js +19 -1
  294. package/neural-interface/public/shared/install-details-view.js +86 -0
  295. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  296. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  297. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
  298. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  299. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
  300. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
  301. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  302. package/neural-interface/public/shared/pwa-install-page.js +32 -2
  303. package/neural-interface/public/shared/pwa-install.css +3 -3
  304. package/neural-interface/public/shared/pwa-install.js +100 -1
  305. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  306. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  307. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  308. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  309. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  310. package/neural-interface/public/shared/settings/settings-ia.js +33 -4
  311. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  312. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  313. package/neural-interface/public/shared/start-bridge.js +238 -19
  314. package/neural-interface/public/shared/storage.js +30 -3
  315. package/neural-interface/public/shared/styles.css +1710 -770
  316. package/neural-interface/public/shared/synabun-mascot.js +68 -58
  317. package/neural-interface/public/shared/tunnel-view.js +167 -0
  318. package/neural-interface/public/shared/ui-claude-panel.js +369 -229
  319. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  320. package/neural-interface/public/shared/ui-detail.js +15 -0
  321. package/neural-interface/public/shared/ui-explorer.js +22 -0
  322. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  323. package/neural-interface/public/shared/ui-file-links.js +103 -0
  324. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  325. package/neural-interface/public/shared/ui-help.js +26 -0
  326. package/neural-interface/public/shared/ui-invite.js +126 -116
  327. package/neural-interface/public/shared/ui-judgments.js +10 -1
  328. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  329. package/neural-interface/public/shared/ui-loading.js +187 -19
  330. package/neural-interface/public/shared/ui-menubar.js +25 -33
  331. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  332. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  333. package/neural-interface/public/shared/ui-navbar.js +48 -22
  334. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  335. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  336. package/neural-interface/public/shared/ui-sessions.js +9 -3
  337. package/neural-interface/public/shared/ui-settings.js +509 -490
  338. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  339. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  340. package/neural-interface/public/shared/ui-sync.js +22 -0
  341. package/neural-interface/public/shared/ui-terminal.js +40 -5
  342. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  343. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  344. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  345. package/neural-interface/public/shared/ui-update.js +594 -5
  346. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  347. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  348. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  349. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  350. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  351. package/neural-interface/public/shared/worker-end.js +253 -0
  352. package/neural-interface/public/sw.js +2 -2
  353. package/neural-interface/public/variant/2d/main.js +10 -1
  354. package/neural-interface/public/variant/3d/main.js +10 -1
  355. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  356. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  357. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  358. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  359. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  360. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  361. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  362. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  363. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  364. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  365. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  366. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  367. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  368. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  369. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  370. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  371. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  372. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  373. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  374. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  375. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  376. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  377. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  378. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  379. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  380. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  381. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  382. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  383. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  384. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  385. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  386. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  387. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  388. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  389. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  390. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  391. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  392. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  393. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  394. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  395. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  396. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  397. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  398. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  399. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  400. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  401. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  402. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  403. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  404. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  405. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  406. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  407. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  408. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  409. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  410. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  411. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  412. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  413. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  414. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  415. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  416. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  417. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  418. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  419. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  420. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  421. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  422. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  423. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  424. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  425. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  426. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  427. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  428. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  429. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  430. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  431. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  432. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  433. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  434. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  435. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  436. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  437. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  438. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  439. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  440. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  441. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  442. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  443. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  444. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  445. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  446. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  447. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  448. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  449. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  450. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  451. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  452. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  453. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  454. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  455. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  456. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  457. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  458. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  459. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  460. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  461. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  462. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  463. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  464. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  465. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  466. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  467. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  468. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  469. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  470. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  471. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  472. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  473. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  474. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  475. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  476. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  477. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  478. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  479. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  480. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  481. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  482. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  483. package/neural-interface/scripts/build-vendor.mjs +78 -2
  484. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  485. package/neural-interface/server.js +2085 -1629
  486. package/neural-interface/templates/rulesets/README.md +1 -1
  487. package/package.json +5 -8
  488. package/setup.js +53 -15
  489. package/benchmarks/browser-live-results.json +0 -79
  490. package/benchmarks/browser-v2-results.json +0 -258
  491. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  492. package/neural-interface/public/category-logos/claude.svg +0 -1
  493. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  494. package/neural-interface/public/category-logos/openai.svg +0 -1
  495. package/neural-interface/public/category-logos/synabun.png +0 -0
  496. package/neural-interface/public/claude.svg +0 -1
  497. package/neural-interface/public/gallery.svg +0 -6
  498. package/neural-interface/public/leonardoai.svg +0 -1
  499. package/neural-interface/public/mcp.svg +0 -4
  500. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  501. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  502. package/neural-interface/public/sessionicon.svg +0 -8
  503. package/neural-interface/public/toolbar-album.svg +0 -8
  504. package/neural-interface/public/toolbar-question.svg +0 -6
  505. package/neural-interface/public/toolbar-sessions.svg +0 -6
  506. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  507. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,533 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — cloudflared: fetch, check, unpack, put in place
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // How a pinned cloudflared (pins.js) gets onto a disk. One sequence, used by
6
+ // the running application (manager.js, into the data home) and, from the
7
+ // unpacking on, by an application build (packaging/lib/stage.mjs):
8
+ //
9
+ // downloadPinned the release file, over HTTPS and nothing else (every
10
+ // redirect too), into a file of its own; stopped at the
11
+ // pinned size, then compared with the pinned SHA-256
12
+ // binaryFromRelease the cloudflared of that file: the file itself, or the
13
+ // one member of the archive, read out by this module
14
+ // (no tar program is started, no name in the archive is
15
+ // ever used as a path); compared with the pinned SHA-256
16
+ // of the binary, then made executable
17
+ // installCloudflared both, in a folder of its own beside the destination,
18
+ // and one rename at the end: the destination holds the
19
+ // whole checked binary or nothing
20
+ //
21
+ // A file that fails a check is removed in the same step and never renamed into
22
+ // place; whatever a stopped process left behind is removed by the next attempt
23
+ // (sweepStaleWork). Every failure is a CloudflaredError with a `code` the page
24
+ // has words for.
25
+ //
26
+ // SynaBun's own folder for its copy (DATA_HOME/runtime/cloudflared) is one only
27
+ // its owner can open (0700, ownFolder); nothing is put in it through a link. A
28
+ // copy that is about to run is checked where it lies (unsafePlace: no link, no
29
+ // folder others may write) and hashed through one handle opened without
30
+ // following a link (checkCloudflared), which says what file it read
31
+ // (fileIdentity): manager.js compares that with the file once more as it is
32
+ // started.
33
+ //
34
+ // Only node: modules here (see pins.js).
35
+
36
+ import { createHash, randomBytes } from 'node:crypto';
37
+ import { chmodSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
38
+ import { copyFile, lstat, open } from 'node:fs/promises';
39
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
40
+ import { createGunzip } from 'node:zlib';
41
+
42
+ /** What can go wrong, by the name the page shows a sentence for. */
43
+ export const CLOUDFLARED_ERROR_CODES = Object.freeze([
44
+ 'unsupported', // Cloudflare publishes no cloudflared for this system
45
+ 'offline', // the download server was not reached
46
+ 'http', // it answered, but not with the file
47
+ 'interrupted', // the transfer ended before the file did
48
+ 'timeout', // nothing arrived for too long
49
+ 'checksum', // what arrived is not the pinned file
50
+ 'unpack', // the archive does not hold the pinned binary
51
+ 'disk', // the data home could not be written (no room, no permission)
52
+ 'cancelled', // stopped on request
53
+ ]);
54
+
55
+ export class CloudflaredError extends Error {
56
+ /** @param {string} code one of CLOUDFLARED_ERROR_CODES */
57
+ constructor(code, message, { cause, detail } = {}) {
58
+ super(message, cause ? { cause } : undefined);
59
+ this.name = 'CloudflaredError';
60
+ this.code = code;
61
+ if (detail) this.detail = detail;
62
+ }
63
+ }
64
+
65
+ const WORK_PREFIX = '.work-';
66
+ const STALE_WORK_MS = 60 * 60 * 1000;
67
+ const MAX_REDIRECTS = 5;
68
+ const DISK_CODES = new Set(['ENOSPC', 'EACCES', 'EPERM', 'EROFS', 'EDQUOT', 'EMFILE', 'ENFILE', 'ENOTDIR', 'EISDIR', 'EEXIST']);
69
+ const NETWORK_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'CERT_HAS_EXPIRED', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'ERR_TLS_CERT_ALTNAME_INVALID']);
70
+
71
+ const codeOf = (error) => error?.code || error?.cause?.code || error?.cause?.cause?.code || '';
72
+
73
+ /** A failure as a CloudflaredError: what it already is, or what its cause says it is, or `fallback`. */
74
+ function asCloudflaredError(error, fallback, message) {
75
+ if (error instanceof CloudflaredError) return error;
76
+ const code = codeOf(error);
77
+ if (DISK_CODES.has(code)) return new CloudflaredError('disk', `The data folder could not be written (${code}).`, { cause: error, detail: code });
78
+ if (NETWORK_CODES.has(code)) return new CloudflaredError('offline', `The download server could not be reached (${code}).`, { cause: error, detail: code });
79
+ return new CloudflaredError(fallback, message, { cause: error, detail: code || error?.message || String(error) });
80
+ }
81
+
82
+ /** SHA-256 and size of a file, read as a stream. */
83
+ export function hashFile(path, { signal } = {}) {
84
+ return new Promise((done, fail) => {
85
+ const hash = createHash('sha256');
86
+ let size = 0;
87
+ const stream = createReadStream(path, { signal });
88
+ stream.on('data', (chunk) => { hash.update(chunk); size += chunk.length; });
89
+ stream.on('error', fail);
90
+ stream.on('end', () => done({ sha256: hash.digest('hex'), size }));
91
+ });
92
+ }
93
+
94
+ /** SHA-256 and size of what an open file holds, read from its start. */
95
+ async function hashHandle(handle, { signal } = {}) {
96
+ const hash = createHash('sha256');
97
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
98
+ let size = 0;
99
+ for (;;) {
100
+ if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.');
101
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, size);
102
+ if (!bytesRead) break;
103
+ hash.update(buffer.subarray(0, bytesRead));
104
+ size += bytesRead;
105
+ }
106
+ return { sha256: hash.digest('hex'), size };
107
+ }
108
+
109
+ /** Opening a file without following a link at its last part, where the system has that (not Windows: asked with lstat first). */
110
+ const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
111
+
112
+ /**
113
+ * What a file is, from a stat taken with `{ bigint: true }`: the same identity is the same file with the same bytes
114
+ * (any write changes its change time, and no program can set that back).
115
+ */
116
+ export function fileIdentity(info) {
117
+ return { dev: info.dev, ino: info.ino, size: info.size, mtimeNs: info.mtimeNs, ctimeNs: info.ctimeNs };
118
+ }
119
+
120
+ /** Are two fileIdentity() the same file, unchanged? */
121
+ export function sameFile(a, b) {
122
+ return !!a && !!b && a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
123
+ }
124
+
125
+ /**
126
+ * Is the file at `path` the pinned binary? Its size first, then its SHA-256, read through one handle opened without
127
+ * following a link at `path` (a link there is `link`, never what it points to). The file is compared before and
128
+ * after the read, and with what `path` names once it is read: `changed` when either differs. `file` is the
129
+ * identity (fileIdentity) of the file the handle opened, whose bytes were read (or could not be: `unreadable`).
130
+ * @returns {Promise<{ ok: boolean, reason: 'missing' | 'link' | 'not-a-file' | 'size' | 'sha256' | 'changed' | 'unreadable' | null, file?: object }>}
131
+ */
132
+ export async function checkCloudflared(path, pin, { signal } = {}) {
133
+ let handle;
134
+ try {
135
+ if (!NO_FOLLOW && (await lstat(path)).isSymbolicLink()) return { ok: false, reason: 'link' };
136
+ handle = await open(path, constants.O_RDONLY | NO_FOLLOW);
137
+ } catch (error) {
138
+ const code = error?.code;
139
+ if (code === 'ENOENT' || code === 'ENOTDIR') return { ok: false, reason: 'missing' };
140
+ if (code === 'ELOOP' || code === 'EMLINK') return { ok: false, reason: 'link' };
141
+ if (code === 'EISDIR') return { ok: false, reason: 'not-a-file' };
142
+ return { ok: false, reason: 'unreadable' };
143
+ }
144
+ try {
145
+ const before = await handle.stat({ bigint: true });
146
+ if (!before.isFile()) return { ok: false, reason: 'not-a-file' };
147
+ const file = fileIdentity(before);
148
+ if (before.size !== BigInt(pin.binary.size)) return { ok: false, reason: 'size', file };
149
+ let sha256;
150
+ try {
151
+ ({ sha256 } = await hashHandle(handle, { signal }));
152
+ } catch (error) {
153
+ if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.', { cause: error });
154
+ return { ok: false, reason: 'unreadable', file };
155
+ }
156
+ // Written to while it was read, or no longer the file `path` names: what was read is not what is there.
157
+ const after = fileIdentity(await handle.stat({ bigint: true }));
158
+ let named = null;
159
+ try { named = fileIdentity(await lstat(path, { bigint: true })); } catch { /* gone meanwhile */ }
160
+ if (!sameFile(file, after) || !sameFile(file, named)) return { ok: false, reason: 'changed', file };
161
+ return sha256 === pin.binary.sha256 ? { ok: true, reason: null, file } : { ok: false, reason: 'sha256', file };
162
+ } finally {
163
+ await handle.close().catch(() => {});
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Is the file at `path` the pinned binary? checkCloudflared, without the identity.
169
+ * @returns {Promise<{ ok: boolean, reason: 'missing' | 'link' | 'not-a-file' | 'size' | 'sha256' | 'changed' | 'unreadable' | null }>}
170
+ */
171
+ export async function verifyCloudflared(path, pin, options) {
172
+ const { ok, reason } = await checkCloudflared(path, pin, options);
173
+ return { ok, reason };
174
+ }
175
+
176
+ /**
177
+ * Is `path`, a cloudflared in SynaBun's own folder `area`, kept where nobody else can change it? Each step from the
178
+ * file up to `area` (both included) is looked at without following a link. null when every one is safe; otherwise
179
+ * the first that is not, `{ reason, at }`: `missing`, `link`, `not-a-file`, `not-a-folder`, `writable` (its group
180
+ * or everybody may write it), `owner` (it belongs to a user other than this one and root), `outside` (`path` is not
181
+ * inside `area`). Permissions are POSIX ones: on Windows only links are looked for. Nothing above `area` is asked:
182
+ * that is the data home, or the installed application, which have to be the user's anyway.
183
+ */
184
+ export function unsafePlace(path, area, options) {
185
+ return lookAtPlace(path, area, options).unsafe;
186
+ }
187
+
188
+ /**
189
+ * unsafePlace, with what that look saw of the file itself: `{ unsafe, file }`, `unsafe` what unsafePlace says and
190
+ * `file` the identity (fileIdentity) of what is at `path`, its last part not followed, from the very lstat that
191
+ * judged it; null when nothing is there. A copy refused for where it lies is known by it (manager.js), never by a
192
+ * second look at its path.
193
+ */
194
+ export function lookAtPlace(path, area, { platform = process.platform, uid = process.getuid?.() } = {}) {
195
+ const top = resolve(area);
196
+ const file = resolve(path);
197
+ const inside = relative(top, file);
198
+ if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)) return { unsafe: { reason: 'outside', at: file }, file: null };
199
+ let seen = null;
200
+ const refused = (reason, at) => ({ unsafe: { reason, at }, file: seen });
201
+ for (let at = file; ;) {
202
+ let info;
203
+ try { info = lstatSync(at, { bigint: true }); } catch { return refused('missing', at); }
204
+ if (at === file) seen = fileIdentity(info);
205
+ if (info.isSymbolicLink()) return refused('link', at);
206
+ if (at === file ? !info.isFile() : !info.isDirectory()) return refused(at === file ? 'not-a-file' : 'not-a-folder', at);
207
+ if (platform !== 'win32') {
208
+ if (Number(info.mode) & 0o022) return refused('writable', at);
209
+ const owner = Number(info.uid);
210
+ if (uid !== undefined && owner !== uid && owner !== 0) return refused('owner', at);
211
+ }
212
+ if (at === top) return { unsafe: null, file: seen };
213
+ const up = dirname(at);
214
+ if (up === at) return refused('outside', at);
215
+ at = up;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * `path` as one of SynaBun's own folders for cloudflared, which only its owner can open (0700): made when it is not
221
+ * there, set back to 0700 when it is this user's and lets anybody else in. A link, something that is not a folder,
222
+ * or another user's folder is refused (`disk`): nothing is put through it, and nothing in it is touched.
223
+ */
224
+ export function ownFolder(path, { platform = process.platform, uid = process.getuid?.() } = {}) {
225
+ try { mkdirSync(path, { mode: 0o700 }); } catch (error) { if (error?.code !== 'EEXIST') throw error; }
226
+ const info = lstatSync(path);
227
+ if (info.isSymbolicLink() || !info.isDirectory()) {
228
+ throw new CloudflaredError('disk', `${path} is ${info.isSymbolicLink() ? 'a link' : 'not a folder'}, not a folder of SynaBun's own; nothing was put there.`, { detail: info.isSymbolicLink() ? 'ELOOP' : 'ENOTDIR' });
229
+ }
230
+ if (platform === 'win32') return path;
231
+ if (uid !== undefined && info.uid !== uid) throw new CloudflaredError('disk', `${path} belongs to another user; nothing was put there.`, { detail: 'EPERM' });
232
+ if (info.mode & 0o077) chmodSync(path, 0o700);
233
+ return path;
234
+ }
235
+
236
+ async function writeAll(handle, buffer) {
237
+ let at = 0;
238
+ while (at < buffer.length) at += (await handle.write(buffer, at, buffer.length - at)).bytesWritten;
239
+ }
240
+
241
+ /** The next address of a redirect, which has to be HTTPS like the first one. */
242
+ function redirectTarget(response, from) {
243
+ const location = response.headers.get('location');
244
+ let next;
245
+ try { next = new URL(location, from); } catch { next = null; }
246
+ if (!location || !next) throw new CloudflaredError('http', 'The download server sent a redirect without an address.', { detail: String(response.status) });
247
+ if (next.protocol !== 'https:') throw new CloudflaredError('http', 'The download server redirected to an address that is not HTTPS; it was not followed.', { detail: next.protocol });
248
+ return next;
249
+ }
250
+
251
+ /**
252
+ * Download a pinned release file into `destination` (a new file: it must not exist) and check it.
253
+ * Resolves `{ sha256, size }` of what is then there, which is the pinned file; anything else rejects, and
254
+ * `destination` is removed before it does.
255
+ *
256
+ * @param {object} o
257
+ * @param {object} o.pin a pin of pins.js
258
+ * @param {string} o.destination
259
+ * @param {typeof fetch} [o.fetchImpl] the transport (tests stand a local server in for GitHub through it)
260
+ * @param {AbortSignal} [o.signal] stops the download: `cancelled`
261
+ * @param {(progress: { received: number, total: number }) => void} [o.onProgress]
262
+ * @param {number} [o.stallMs] how long nothing may arrive, headers included
263
+ */
264
+ export async function downloadPinned({ pin, destination, fetchImpl = fetch, signal, onProgress = () => {}, stallMs = 45_000 }) {
265
+ if (!pin?.url?.startsWith('https://')) throw new CloudflaredError('http', 'The pinned address is not HTTPS; nothing was downloaded.');
266
+ const stop = new AbortController();
267
+ const onAbort = () => stop.abort(new CloudflaredError('cancelled', 'The download was stopped.'));
268
+ if (signal?.aborted) onAbort(); else signal?.addEventListener('abort', onAbort, { once: true });
269
+ let timer = null;
270
+ const alive = () => {
271
+ clearTimeout(timer);
272
+ timer = setTimeout(() => stop.abort(new CloudflaredError('timeout', `Nothing arrived from the download server for ${Math.round(stallMs / 1000)} seconds.`)), stallMs);
273
+ };
274
+ let handle = null;
275
+ let made = false; // the destination is this call's own file: only then is it removed on a failure
276
+ let answered = false; // the server sent the file's headers: what fails after that is the transfer
277
+ let received = 0;
278
+ const hash = createHash('sha256');
279
+ try {
280
+ alive();
281
+ let url = new URL(pin.url);
282
+ let response = null;
283
+ for (let hop = 0; ; hop++) {
284
+ // identity: the file's bytes as they are, so that their number is the pinned size
285
+ response = await fetchImpl(url, { redirect: 'manual', signal: stop.signal, headers: { accept: 'application/octet-stream', 'accept-encoding': 'identity', 'user-agent': 'synabun' } });
286
+ if (response.status < 300 || response.status >= 400) break;
287
+ const next = redirectTarget(response, url);
288
+ try { await response.body?.cancel(); } catch { /* nothing to drain */ }
289
+ if (hop >= MAX_REDIRECTS) throw new CloudflaredError('http', 'The download server redirected too many times.');
290
+ url = next;
291
+ alive();
292
+ }
293
+ if (response.status !== 200 || !response.body) {
294
+ try { await response.body?.cancel(); } catch { /* nothing to drain */ }
295
+ throw new CloudflaredError('http', `The download server answered ${response.status} instead of the file.`, { detail: String(response.status) });
296
+ }
297
+ const announced = Number(response.headers.get('content-length'));
298
+ if (Number.isFinite(announced) && announced > 0 && announced !== pin.size) {
299
+ try { await response.body.cancel(); } catch { /* nothing to drain */ }
300
+ throw new CloudflaredError('checksum', `The download server offered a file of ${announced} bytes; the pinned one has ${pin.size}. It was not downloaded.`);
301
+ }
302
+ answered = true;
303
+ handle = await open(destination, 'wx', 0o600);
304
+ made = true;
305
+ onProgress({ received: 0, total: pin.size });
306
+ for await (const chunk of response.body) {
307
+ alive();
308
+ received += chunk.length;
309
+ if (received > pin.size) throw new CloudflaredError('checksum', 'The download is larger than the pinned file. It was stopped and removed.');
310
+ hash.update(chunk);
311
+ await writeAll(handle, chunk);
312
+ onProgress({ received, total: pin.size });
313
+ }
314
+ if (received !== pin.size) throw new CloudflaredError('interrupted', `The download ended after ${received} of ${pin.size} bytes.`);
315
+ const sha256 = hash.digest('hex');
316
+ if (sha256 !== pin.sha256) throw new CloudflaredError('checksum', 'The downloaded file does not match its pinned checksum. It was removed and will never be run.', { detail: sha256 });
317
+ await handle.sync();
318
+ await handle.close();
319
+ handle = null;
320
+ return { sha256, size: received };
321
+ } catch (error) {
322
+ try { await handle?.close(); } catch { /* already closed */ }
323
+ handle = null;
324
+ if (made) rmSync(destination, { force: true });
325
+ // An abort arrives as whatever the transport makes of it: the reason it was given is the answer.
326
+ if (stop.signal.aborted && stop.signal.reason instanceof CloudflaredError) throw stop.signal.reason;
327
+ if (error instanceof CloudflaredError) throw error;
328
+ if (DISK_CODES.has(codeOf(error))) throw asCloudflaredError(error, 'disk', 'The data folder could not be written.');
329
+ if (answered) throw new CloudflaredError('interrupted', `The download ended after ${received} of ${pin.size} bytes.`, { cause: error, detail: codeOf(error) || error?.message });
330
+ throw asCloudflaredError(error, 'offline', 'The download server could not be reached.');
331
+ } finally {
332
+ clearTimeout(timer);
333
+ signal?.removeEventListener('abort', onAbort);
334
+ }
335
+ }
336
+
337
+ /** One header block of a tar archive: the entry's name, the size of its data, and whether it is a plain file. */
338
+ function readTarHeader(block) {
339
+ const text = (from, to) => { const end = block.indexOf(0, from); return block.toString('utf8', from, end === -1 || end > to ? to : end); };
340
+ const stored = parseInt(text(148, 156).trim(), 8);
341
+ let sum = 0;
342
+ for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : block[i];
343
+ if (stored !== sum) throw new CloudflaredError('unpack', 'The archive is not a tar archive.');
344
+ let size = 0;
345
+ if (block[124] & 0x80) for (let i = 125; i < 136; i++) size = size * 256 + block[i]; // base-256, for sizes octal cannot hold
346
+ else size = parseInt(text(124, 136).trim() || '0', 8);
347
+ if (!Number.isSafeInteger(size) || size < 0) throw new CloudflaredError('unpack', 'The archive names an entry without a size.');
348
+ const prefix = text(345, 500);
349
+ const name = `${prefix ? `${prefix}/` : ''}${text(0, 100)}`.replace(/^\.\//, '');
350
+ const flag = block[156];
351
+ return { name, size, file: flag === 0 || flag === 48 }; // "\0" and "0": a regular file
352
+ }
353
+
354
+ /**
355
+ * Read one member out of a gzipped tar archive into `destination` (a new file), without a tar program and
356
+ * without ever using a name of the archive as a path: the entry called `member` that is a regular file of
357
+ * exactly `size` bytes is copied out, and nothing else is looked at. Resolves `{ sha256, size }` of what was
358
+ * written; rejects `unpack` (and removes `destination`) when the archive holds no such entry or ends inside it.
359
+ */
360
+ export async function extractTarMember({ archive, member, size, destination, signal }) {
361
+ const hash = createHash('sha256');
362
+ let source = null;
363
+ let gunzip = null;
364
+ let handle = null;
365
+ let made = false;
366
+ let pending = Buffer.alloc(0);
367
+ let mode = 'header'; // header | member | skip | done | end
368
+ let left = 0; // bytes of the current entry still to come (its padding included, when skipping)
369
+ let padding = 0;
370
+ let written = 0;
371
+ try {
372
+ handle = await open(destination, 'wx', 0o600);
373
+ made = true;
374
+ // Made here, with nothing awaited before the loop that reads them: an archive that cannot be opened fails
375
+ // the loop. (The listener on gunzip is for the instant before the loop has its own: an 'error' nobody
376
+ // listens for would be an uncaught exception of the whole server.)
377
+ source = createReadStream(archive);
378
+ gunzip = createGunzip();
379
+ gunzip.on('error', () => {});
380
+ source.on('error', (error) => gunzip.destroy(error));
381
+ for await (const chunk of source.pipe(gunzip)) {
382
+ if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.');
383
+ pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
384
+ while (pending.length && mode !== 'done') {
385
+ if (mode === 'header') {
386
+ if (pending.length < 512) break;
387
+ const block = pending.subarray(0, 512);
388
+ pending = pending.subarray(512);
389
+ if (block.every((byte) => byte === 0)) { mode = 'end'; break; }
390
+ const entry = readTarHeader(block);
391
+ padding = (512 - (entry.size % 512)) % 512;
392
+ if (entry.file && entry.name === member) {
393
+ if (entry.size !== size) throw new CloudflaredError('unpack', `The archive's ${member} has ${entry.size} bytes; the pinned one has ${size}.`);
394
+ mode = 'member';
395
+ left = entry.size;
396
+ } else {
397
+ mode = entry.size + padding ? 'skip' : 'header';
398
+ left = entry.size + padding;
399
+ }
400
+ } else {
401
+ const take = Math.min(left, pending.length);
402
+ if (mode === 'member') {
403
+ const part = pending.subarray(0, take);
404
+ hash.update(part);
405
+ await writeAll(handle, part);
406
+ written += take;
407
+ }
408
+ pending = pending.subarray(take);
409
+ left -= take;
410
+ if (left === 0) mode = mode === 'member' ? 'done' : 'header';
411
+ }
412
+ }
413
+ if (mode === 'done' || mode === 'end') break;
414
+ }
415
+ if (mode !== 'done') throw new CloudflaredError('unpack', mode === 'member' ? `The archive ends inside its ${member}.` : `The archive holds no ${member}.`);
416
+ await handle.sync();
417
+ await handle.close();
418
+ handle = null;
419
+ return { sha256: hash.digest('hex'), size: written };
420
+ } catch (error) {
421
+ try { await handle?.close(); } catch { /* already closed */ }
422
+ handle = null;
423
+ if (made) rmSync(destination, { force: true });
424
+ throw asCloudflaredError(error, 'unpack', 'The archive could not be read.');
425
+ } finally {
426
+ source?.destroy();
427
+ gunzip?.destroy();
428
+ }
429
+ }
430
+
431
+ /**
432
+ * The cloudflared of a release file that was already checked, as a new executable file at `destination`:
433
+ * the file itself (moved there with `move`, copied otherwise), or the member of the archive. What ends up
434
+ * there is compared with the pinned SHA-256 of the binary; a file that is not it is removed and `checksum`
435
+ * (or `unpack`) is thrown.
436
+ */
437
+ export async function binaryFromRelease({ pin, releaseFile, destination, move = false, signal, platform = process.platform }) {
438
+ let made = false; // the destination is this call's own file from here on: only then is it removed on a failure
439
+ try {
440
+ if (existsSync(destination)) throw new CloudflaredError('disk', 'Something is already where cloudflared was to be put; it was left as it is.', { detail: 'EEXIST' });
441
+ if (pin.archive === 'tgz') {
442
+ const unpacked = await extractTarMember({ archive: releaseFile, member: pin.member, size: pin.binary.size, destination, signal });
443
+ made = true;
444
+ if (unpacked.sha256 !== pin.binary.sha256) throw new CloudflaredError('checksum', 'The cloudflared inside the archive does not match its pinned checksum. It was removed and will never be run.', { detail: unpacked.sha256 });
445
+ } else {
446
+ if (move) renameSync(releaseFile, destination);
447
+ else await copyFile(releaseFile, destination, constants.COPYFILE_EXCL);
448
+ made = true;
449
+ const check = await verifyCloudflared(destination, pin, { signal });
450
+ if (!check.ok) throw new CloudflaredError('checksum', 'The file does not match the pinned checksum of cloudflared. It was removed and will never be run.', { detail: check.reason });
451
+ }
452
+ if (platform !== 'win32') chmodSync(destination, 0o755);
453
+ return destination;
454
+ } catch (error) {
455
+ if (made) rmSync(destination, { force: true });
456
+ throw asCloudflaredError(error, 'disk', 'cloudflared could not be put in place.');
457
+ }
458
+ }
459
+
460
+ const processAlive = (pid) => {
461
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
462
+ };
463
+
464
+ /**
465
+ * Remove the work folders an earlier attempt left in `root` (a process that was stopped mid-download cannot
466
+ * clean up after itself): every one whose process is gone or this process itself, and any older than an hour.
467
+ * @returns {string[]} what was removed
468
+ */
469
+ export function sweepStaleWork(root, { now = Date.now(), alive = processAlive, pid = process.pid } = {}) {
470
+ const removed = [];
471
+ let names = [];
472
+ try { names = readdirSync(root); } catch { return removed; }
473
+ for (const name of names) {
474
+ if (!name.startsWith(WORK_PREFIX)) continue;
475
+ const owner = Number(name.slice(WORK_PREFIX.length).split('-')[0]);
476
+ const path = join(root, name);
477
+ let age = Infinity;
478
+ try { age = now - statSync(path).mtimeMs; } catch { /* gone meanwhile */ }
479
+ if (owner === pid || !Number.isInteger(owner) || !alive(owner) || age > STALE_WORK_MS) {
480
+ try { rmSync(path, { recursive: true, force: true }); removed.push(name); } catch { /* in use: the next attempt asks again */ }
481
+ }
482
+ }
483
+ return removed;
484
+ }
485
+
486
+ /**
487
+ * Download the pinned cloudflared and put it at `<root>/<version>/cloudflared[.exe]`. Everything happens in a
488
+ * work folder of this attempt inside `root`, which is removed whatever the outcome; the binary is renamed into
489
+ * place only after both of its checks, so its place holds the whole checked file or nothing. `root` and the
490
+ * version's folder are SynaBun's own, 0700 (ownFolder): a link or another user's folder there stops the install.
491
+ * One attempt at a time per process is the caller's business (manager.js); two processes that download at once
492
+ * each rename a checked file into the same place, and the place holds one of them, whole.
493
+ *
494
+ * @returns {Promise<{ path: string, version: string }>}
495
+ */
496
+ export async function installCloudflared({ pin, root, fetchImpl, signal, onProgress, stallMs, platform = process.platform }) {
497
+ const directory = join(root, pin.version);
498
+ const destination = join(directory, pin.binary.name);
499
+ let work = null;
500
+ let placed = false;
501
+ try {
502
+ mkdirSync(dirname(root), { recursive: true });
503
+ ownFolder(root);
504
+ sweepStaleWork(root);
505
+ work = join(root, `${WORK_PREFIX}${process.pid}-${randomBytes(6).toString('hex')}`);
506
+ mkdirSync(work, { mode: 0o700 });
507
+ const releaseFile = join(work, 'release.part');
508
+ await downloadPinned({ pin, destination: releaseFile, fetchImpl, signal, onProgress, stallMs });
509
+ const binary = join(work, 'binary.part');
510
+ await binaryFromRelease({ pin, releaseFile, destination: binary, move: true, signal, platform });
511
+ if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.');
512
+ // A link or a file where the version's folder belongs is taken away (the link itself, never what it points to).
513
+ let there = null;
514
+ try { there = lstatSync(directory); } catch { /* not there yet */ }
515
+ if (there && (there.isSymbolicLink() || !there.isDirectory())) { try { unlinkSync(directory); } catch { rmdirSync(directory); } }
516
+ ownFolder(directory);
517
+ try {
518
+ renameSync(binary, destination);
519
+ } catch (error) {
520
+ // Windows does not replace a file that is running: when what is there is the pinned binary, it is the result.
521
+ if (!existsSync(destination) || !(await verifyCloudflared(destination, pin)).ok) throw error;
522
+ }
523
+ placed = true;
524
+ return { path: destination, version: pin.version };
525
+ } catch (error) {
526
+ throw asCloudflaredError(error, 'disk', 'cloudflared could not be put in place.');
527
+ } finally {
528
+ if (work) { try { rmSync(work, { recursive: true, force: true }); } catch { /* the next attempt sweeps it */ } }
529
+ // A failed attempt leaves no folder of its own either (rmdir only ever removes an empty one). A `root` that was
530
+ // not SynaBun's own (no work folder was made in it) is left as it is.
531
+ if (!placed && work) for (const folder of [directory, root]) { try { rmdirSync(folder); } catch { /* not empty, or not there */ } }
532
+ }
533
+ }