@thevinci/web 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/README.md +197 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +4887 -0
  5. package/bin/cli.test.js +64 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +528 -0
  12. package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
  13. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
  73. package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
  74. package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
  75. package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
  76. package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
  77. package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
  78. package/dist/assets/es-4o9ciP61.js +15 -0
  79. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  80. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  81. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  82. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  83. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  84. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  85. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  86. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  87. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  88. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  89. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  90. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  91. package/dist/assets/index-DLTDToSP.css +1 -0
  92. package/dist/assets/index-DgiFEKGN.js +1 -0
  93. package/dist/assets/ko-B20imCHE.js +15 -0
  94. package/dist/assets/main-BV3KOtdA.css +1 -0
  95. package/dist/assets/main-CDKJj0sH.js +226 -0
  96. package/dist/assets/main-LC-PSNVM.js +2 -0
  97. package/dist/assets/miniChat-CQUiG_cr.js +2 -0
  98. package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
  99. package/dist/assets/pl-DQJ7LSzj.js +15 -0
  100. package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
  101. package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
  102. package/dist/assets/uk-BNFxOlO4.js +15 -0
  103. package/dist/assets/vendor--DBfsbEis.css +1 -0
  104. package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
  105. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  106. package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
  107. package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
  108. package/dist/assets/worker-CbT6TVo7.js +155 -0
  109. package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
  110. package/dist/favicon-16.png +0 -0
  111. package/dist/favicon-32.png +0 -0
  112. package/dist/favicon.png +0 -0
  113. package/dist/favicon.svg +528 -0
  114. package/dist/index.html +607 -0
  115. package/dist/logo-dark-192x192.png +0 -0
  116. package/dist/logo-dark-512x512.png +0 -0
  117. package/dist/logo-dark-512x512.svg +528 -0
  118. package/dist/logo-light-192x192.png +0 -0
  119. package/dist/logo-light-512x512.png +0 -0
  120. package/dist/logo-light-512x512.svg +528 -0
  121. package/dist/mini-chat.html +16 -0
  122. package/dist/pwa-192.png +0 -0
  123. package/dist/pwa-512.png +0 -0
  124. package/dist/pwa-maskable-192.png +0 -0
  125. package/dist/pwa-maskable-512.png +0 -0
  126. package/dist/site.webmanifest +21 -0
  127. package/dist/sw.js +1 -0
  128. package/package.json +118 -0
  129. package/public/apple-touch-icon-120x120.png +0 -0
  130. package/public/apple-touch-icon-152x152.png +0 -0
  131. package/public/apple-touch-icon-167x167.png +0 -0
  132. package/public/apple-touch-icon-180x180.png +0 -0
  133. package/public/apple-touch-icon.png +0 -0
  134. package/public/apple-touch-icon.svg +528 -0
  135. package/public/favicon-16.png +0 -0
  136. package/public/favicon-32.png +0 -0
  137. package/public/favicon.png +0 -0
  138. package/public/favicon.svg +528 -0
  139. package/public/logo-dark-192x192.png +0 -0
  140. package/public/logo-dark-512x512.png +0 -0
  141. package/public/logo-dark-512x512.svg +528 -0
  142. package/public/logo-light-192x192.png +0 -0
  143. package/public/logo-light-512x512.png +0 -0
  144. package/public/logo-light-512x512.svg +528 -0
  145. package/public/pwa-192.png +0 -0
  146. package/public/pwa-512.png +0 -0
  147. package/public/pwa-maskable-192.png +0 -0
  148. package/public/pwa-maskable-512.png +0 -0
  149. package/public/site.webmanifest +21 -0
  150. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  151. package/server/index.d.ts +39 -0
  152. package/server/index.js +1311 -0
  153. package/server/lib/cloudflare-tunnel.js +650 -0
  154. package/server/lib/event-stream/DOCUMENTATION.md +61 -0
  155. package/server/lib/event-stream/directory-ws-bridge.js +185 -0
  156. package/server/lib/event-stream/global-hub.js +158 -0
  157. package/server/lib/event-stream/global-hub.test.js +140 -0
  158. package/server/lib/event-stream/global-ws-bridge.js +206 -0
  159. package/server/lib/event-stream/index.js +25 -0
  160. package/server/lib/event-stream/protocol.js +131 -0
  161. package/server/lib/event-stream/protocol.test.js +182 -0
  162. package/server/lib/event-stream/runtime.js +180 -0
  163. package/server/lib/event-stream/runtime.test.js +512 -0
  164. package/server/lib/event-stream/upstream-reader.js +226 -0
  165. package/server/lib/event-stream/upstream-reader.test.js +276 -0
  166. package/server/lib/fs/DOCUMENTATION.md +36 -0
  167. package/server/lib/fs/routes.js +1040 -0
  168. package/server/lib/fs/search.js +238 -0
  169. package/server/lib/git/DOCUMENTATION.md +152 -0
  170. package/server/lib/git/credentials.js +74 -0
  171. package/server/lib/git/identity-storage.js +112 -0
  172. package/server/lib/git/index.js +6 -0
  173. package/server/lib/git/routes.js +972 -0
  174. package/server/lib/git/service.js +3432 -0
  175. package/server/lib/git/service.test.js +39 -0
  176. package/server/lib/github/DOCUMENTATION.md +171 -0
  177. package/server/lib/github/auth.js +307 -0
  178. package/server/lib/github/device-flow.js +50 -0
  179. package/server/lib/github/index.js +24 -0
  180. package/server/lib/github/octokit.js +10 -0
  181. package/server/lib/github/pr-status.js +519 -0
  182. package/server/lib/github/repo/fork-detection.js +102 -0
  183. package/server/lib/github/repo/index.js +55 -0
  184. package/server/lib/github/routes.js +1560 -0
  185. package/server/lib/magic-prompts/routes.js +63 -0
  186. package/server/lib/magic-prompts/runtime.js +119 -0
  187. package/server/lib/notifications/DOCUMENTATION.md +122 -0
  188. package/server/lib/notifications/emitter-runtime.js +102 -0
  189. package/server/lib/notifications/index.js +4 -0
  190. package/server/lib/notifications/message.js +52 -0
  191. package/server/lib/notifications/message.test.js +34 -0
  192. package/server/lib/notifications/push-runtime.js +304 -0
  193. package/server/lib/notifications/routes.js +315 -0
  194. package/server/lib/notifications/runtime.js +566 -0
  195. package/server/lib/notifications/template-runtime.js +349 -0
  196. package/server/lib/notifications/template-runtime.test.js +26 -0
  197. package/server/lib/opencode/DOCUMENTATION.md +362 -0
  198. package/server/lib/opencode/agents.js +634 -0
  199. package/server/lib/opencode/auth-state-runtime.js +88 -0
  200. package/server/lib/opencode/auth.js +83 -0
  201. package/server/lib/opencode/bootstrap-runtime.js +131 -0
  202. package/server/lib/opencode/cli-entry-runtime.js +43 -0
  203. package/server/lib/opencode/cli-options.js +128 -0
  204. package/server/lib/opencode/commands.js +339 -0
  205. package/server/lib/opencode/config-entity-routes.js +370 -0
  206. package/server/lib/opencode/core-routes.js +500 -0
  207. package/server/lib/opencode/core-routes.test.js +26 -0
  208. package/server/lib/opencode/env-config.js +74 -0
  209. package/server/lib/opencode/env-keys.js +68 -0
  210. package/server/lib/opencode/env-runtime.js +1162 -0
  211. package/server/lib/opencode/env-runtime.test.js +116 -0
  212. package/server/lib/opencode/feature-routes-runtime.js +244 -0
  213. package/server/lib/opencode/hmr-state-runtime.js +85 -0
  214. package/server/lib/opencode/index.js +66 -0
  215. package/server/lib/opencode/lifecycle.js +1019 -0
  216. package/server/lib/opencode/lifecycle.test.js +240 -0
  217. package/server/lib/opencode/mcp.js +278 -0
  218. package/server/lib/opencode/network-runtime.js +104 -0
  219. package/server/lib/opencode/network-runtime.test.js +37 -0
  220. package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
  221. package/server/lib/opencode/path-utils.js +100 -0
  222. package/server/lib/opencode/path-utils.test.js +71 -0
  223. package/server/lib/opencode/project-directory-runtime.js +124 -0
  224. package/server/lib/opencode/project-icon-routes.js +399 -0
  225. package/server/lib/opencode/project-icon-routes.test.js +107 -0
  226. package/server/lib/opencode/providers.js +96 -0
  227. package/server/lib/opencode/proxy.js +445 -0
  228. package/server/lib/opencode/pwa-manifest-routes.js +257 -0
  229. package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
  230. package/server/lib/opencode/routes.js +541 -0
  231. package/server/lib/opencode/server-startup-runtime.js +156 -0
  232. package/server/lib/opencode/server-utils-runtime.js +168 -0
  233. package/server/lib/opencode/server-utils-runtime.test.js +135 -0
  234. package/server/lib/opencode/session-runtime.js +356 -0
  235. package/server/lib/opencode/session-runtime.test.js +151 -0
  236. package/server/lib/opencode/settings-helpers.js +770 -0
  237. package/server/lib/opencode/settings-helpers.test.js +109 -0
  238. package/server/lib/opencode/settings-normalization-runtime.js +428 -0
  239. package/server/lib/opencode/settings-runtime.js +826 -0
  240. package/server/lib/opencode/settings-runtime.test.js +85 -0
  241. package/server/lib/opencode/shared.js +615 -0
  242. package/server/lib/opencode/shutdown-runtime.js +139 -0
  243. package/server/lib/opencode/shutdown-runtime.test.js +58 -0
  244. package/server/lib/opencode/skill-routes.js +701 -0
  245. package/server/lib/opencode/skills.js +548 -0
  246. package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
  247. package/server/lib/opencode/static-routes-runtime.js +65 -0
  248. package/server/lib/opencode/theme-runtime.js +167 -0
  249. package/server/lib/opencode/tunnel-auth.js +591 -0
  250. package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
  251. package/server/lib/opencode/vinci-routes.js +76 -0
  252. package/server/lib/opencode/watcher.js +115 -0
  253. package/server/lib/opencode/watcher.test.js +239 -0
  254. package/server/lib/preview/proxy-runtime.js +1333 -0
  255. package/server/lib/preview/proxy-runtime.test.js +144 -0
  256. package/server/lib/projects/project-config.js +567 -0
  257. package/server/lib/projects/project-config.test.js +175 -0
  258. package/server/lib/projects/project-id.js +13 -0
  259. package/server/lib/quota/DOCUMENTATION.md +58 -0
  260. package/server/lib/quota/index.js +25 -0
  261. package/server/lib/quota/providers/claude.js +107 -0
  262. package/server/lib/quota/providers/codex.js +113 -0
  263. package/server/lib/quota/providers/copilot.js +165 -0
  264. package/server/lib/quota/providers/google/api.js +92 -0
  265. package/server/lib/quota/providers/google/auth.js +108 -0
  266. package/server/lib/quota/providers/google/index.js +124 -0
  267. package/server/lib/quota/providers/google/transforms.js +109 -0
  268. package/server/lib/quota/providers/index.js +168 -0
  269. package/server/lib/quota/providers/interface.js +55 -0
  270. package/server/lib/quota/providers/kimi.js +108 -0
  271. package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
  272. package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
  273. package/server/lib/quota/providers/nanogpt.js +124 -0
  274. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  275. package/server/lib/quota/providers/openai.js +91 -0
  276. package/server/lib/quota/providers/openrouter.js +92 -0
  277. package/server/lib/quota/providers/zai.js +91 -0
  278. package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
  279. package/server/lib/quota/providers/zhipuai.js +114 -0
  280. package/server/lib/quota/routes.js +27 -0
  281. package/server/lib/quota/utils/auth.js +50 -0
  282. package/server/lib/quota/utils/formatters.js +85 -0
  283. package/server/lib/quota/utils/formatters.test.js +54 -0
  284. package/server/lib/quota/utils/index.js +10 -0
  285. package/server/lib/quota/utils/transformers.js +55 -0
  286. package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
  287. package/server/lib/scheduled-tasks/routes.js +235 -0
  288. package/server/lib/scheduled-tasks/runtime.js +773 -0
  289. package/server/lib/scheduled-tasks/runtime.test.js +100 -0
  290. package/server/lib/security/request-security.js +115 -0
  291. package/server/lib/session-folders/routes.js +63 -0
  292. package/server/lib/session-folders/routes.test.js +102 -0
  293. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  294. package/server/lib/skills-catalog/cache.js +29 -0
  295. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  296. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  297. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  298. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  299. package/server/lib/skills-catalog/curated-sources.js +21 -0
  300. package/server/lib/skills-catalog/git.js +77 -0
  301. package/server/lib/skills-catalog/index.js +42 -0
  302. package/server/lib/skills-catalog/install.js +294 -0
  303. package/server/lib/skills-catalog/scan.js +221 -0
  304. package/server/lib/skills-catalog/source.js +87 -0
  305. package/server/lib/terminal/DOCUMENTATION.md +76 -0
  306. package/server/lib/terminal/index.js +31 -0
  307. package/server/lib/terminal/output-replay-buffer.js +78 -0
  308. package/server/lib/terminal/output-replay-buffer.test.js +75 -0
  309. package/server/lib/terminal/runtime.js +850 -0
  310. package/server/lib/terminal/runtime.test.js +96 -0
  311. package/server/lib/terminal/terminal-ws-protocol.js +68 -0
  312. package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
  313. package/server/lib/text/DOCUMENTATION.md +35 -0
  314. package/server/lib/text/summarization.js +138 -0
  315. package/server/lib/text/summarization.test.js +34 -0
  316. package/server/lib/tts/DOCUMENTATION.md +146 -0
  317. package/server/lib/tts/base-url.js +62 -0
  318. package/server/lib/tts/capability-runtime.js +31 -0
  319. package/server/lib/tts/index.js +19 -0
  320. package/server/lib/tts/routes.js +261 -0
  321. package/server/lib/tts/routes.test.js +53 -0
  322. package/server/lib/tts/service.js +178 -0
  323. package/server/lib/tts/stt.js +75 -0
  324. package/server/lib/tunnels/DOCUMENTATION.md +18 -0
  325. package/server/lib/tunnels/index.js +166 -0
  326. package/server/lib/tunnels/managed-config.js +201 -0
  327. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  328. package/server/lib/tunnels/registry.js +51 -0
  329. package/server/lib/tunnels/routes.js +605 -0
  330. package/server/lib/tunnels/types.js +219 -0
  331. package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
  332. package/server/lib/ui-auth/ui-auth.js +673 -0
  333. package/server/lib/ui-auth/ui-passkeys.js +545 -0
  334. package/server/opencode-proxy.test.js +151 -0
  335. package/server/proxy-headers.js +61 -0
  336. package/server/proxy-headers.test.js +58 -0
  337. package/server/sse-routes.test.js +152 -0
@@ -0,0 +1,605 @@
1
+ export const createTunnelRoutesRuntime = (dependencies) => {
2
+ const {
3
+ crypto,
4
+ URL,
5
+ tunnelService,
6
+ tunnelProviderRegistry,
7
+ tunnelAuthController,
8
+ readSettingsFromDiskMigrated,
9
+ readManagedRemoteTunnelConfigFromDisk,
10
+ normalizeTunnelProvider,
11
+ normalizeTunnelMode,
12
+ normalizeOptionalPath,
13
+ normalizeManagedRemoteTunnelHostname,
14
+ normalizeTunnelBootstrapTtlMs,
15
+ normalizeTunnelSessionTtlMs,
16
+ isSupportedTunnelMode,
17
+ upsertManagedRemoteTunnelToken,
18
+ resolveManagedRemoteTunnelToken,
19
+ TUNNEL_MODE_QUICK,
20
+ TUNNEL_MODE_MANAGED_LOCAL,
21
+ TUNNEL_MODE_MANAGED_REMOTE,
22
+ TUNNEL_PROVIDER_CLOUDFLARE,
23
+ TunnelServiceError,
24
+ getActivePort,
25
+ getRuntimeManagedRemoteTunnelHostname,
26
+ setRuntimeManagedRemoteTunnelHostname,
27
+ getRuntimeManagedRemoteTunnelToken,
28
+ setRuntimeManagedRemoteTunnelToken,
29
+ getActiveTunnelController,
30
+ setActiveTunnelController,
31
+ } = dependencies;
32
+
33
+ const resolveActiveNormalizedTunnelMode = () => {
34
+ const mode = tunnelService.resolveActiveMode();
35
+ if (mode === TUNNEL_MODE_MANAGED_LOCAL) {
36
+ return TUNNEL_MODE_MANAGED_LOCAL;
37
+ }
38
+ if (mode === TUNNEL_MODE_MANAGED_REMOTE) {
39
+ return TUNNEL_MODE_MANAGED_REMOTE;
40
+ }
41
+ return TUNNEL_MODE_QUICK;
42
+ };
43
+
44
+ const resolveNormalizedTunnelHost = (publicUrl) => {
45
+ if (typeof publicUrl !== 'string' || publicUrl.trim().length === 0) {
46
+ return null;
47
+ }
48
+ try {
49
+ return new URL(publicUrl).hostname.toLowerCase();
50
+ } catch {
51
+ return null;
52
+ }
53
+ };
54
+
55
+ const resolvePreferredTunnelProvider = async (reqBody = null) => {
56
+ if (typeof reqBody?.provider === 'string' && reqBody.provider.trim().length > 0) {
57
+ return normalizeTunnelProvider(reqBody.provider);
58
+ }
59
+ const activeProvider = tunnelService.resolveActiveProvider();
60
+ if (activeProvider) {
61
+ return normalizeTunnelProvider(activeProvider);
62
+ }
63
+ const settings = await readSettingsFromDiskMigrated();
64
+ return normalizeTunnelProvider(settings?.tunnelProvider);
65
+ };
66
+
67
+ const startTunnelWithNormalizedRequest = async ({
68
+ provider,
69
+ mode,
70
+ intent,
71
+ hostname,
72
+ token,
73
+ configPath,
74
+ selectedPresetId,
75
+ selectedPresetName,
76
+ }) => {
77
+ if (provider === TUNNEL_PROVIDER_CLOUDFLARE && mode === TUNNEL_MODE_MANAGED_REMOTE) {
78
+ setRuntimeManagedRemoteTunnelHostname(hostname);
79
+ setRuntimeManagedRemoteTunnelToken(token);
80
+
81
+ if (token && hostname) {
82
+ await upsertManagedRemoteTunnelToken({
83
+ id: selectedPresetId || hostname,
84
+ name: selectedPresetName || hostname,
85
+ hostname,
86
+ token,
87
+ });
88
+ }
89
+ }
90
+
91
+ const result = await tunnelService.start({
92
+ provider,
93
+ mode,
94
+ intent,
95
+ configPath,
96
+ token,
97
+ hostname,
98
+ });
99
+
100
+ console.log(`Tunnel active (${result.provider}): ${result.publicUrl}`);
101
+ return {
102
+ publicUrl: result.publicUrl,
103
+ mode: result.activeMode,
104
+ provider: result.provider,
105
+ providerMetadata: result.providerMetadata,
106
+ };
107
+ };
108
+
109
+ const createGenericModeChecks = ({ modeKey, requiredFields, doctorRequest, startupReady }) => {
110
+ const checks = [
111
+ {
112
+ id: 'startup_readiness',
113
+ label: 'Provider startup readiness',
114
+ status: startupReady ? 'pass' : 'fail',
115
+ detail: startupReady
116
+ ? 'Provider dependency checks passed.'
117
+ : 'Resolve provider checks before starting tunnels.',
118
+ },
119
+ ];
120
+
121
+ for (const field of requiredFields) {
122
+ const value = doctorRequest?.[field];
123
+ const present = typeof value === 'string' ? value.trim().length > 0 : Boolean(value);
124
+ checks.push({
125
+ id: `requirement_${field}`,
126
+ label: `Required: ${field}`,
127
+ status: present ? 'pass' : 'fail',
128
+ detail: present
129
+ ? `${field} is configured.`
130
+ : `${field} is required for ${modeKey}.`,
131
+ });
132
+ }
133
+
134
+ const failures = checks.filter((entry) => entry.status === 'fail').length;
135
+ const warnings = checks.filter((entry) => entry.status === 'warn').length;
136
+ return {
137
+ mode: modeKey,
138
+ checks,
139
+ summary: {
140
+ ready: failures === 0,
141
+ failures,
142
+ warnings,
143
+ },
144
+ ready: failures === 0,
145
+ blockers: checks
146
+ .filter((entry) => entry.status === 'fail' && entry.id !== 'startup_readiness')
147
+ .map((entry) => entry.detail || entry.label || entry.id),
148
+ };
149
+ };
150
+
151
+ const runTunnelDoctor = async ({ providerId, modeFilter, doctorRequest }) => {
152
+ const provider = tunnelProviderRegistry.get(providerId);
153
+ if (!provider) {
154
+ throw new TunnelServiceError('provider_unsupported', `Unsupported tunnel provider: ${providerId}`);
155
+ }
156
+
157
+ const capabilities = provider.capabilities || {};
158
+ const modeKeys = Array.isArray(capabilities.modes)
159
+ ? capabilities.modes.map((entry) => entry?.key).filter((key) => typeof key === 'string' && key.length > 0)
160
+ : [];
161
+
162
+ if (modeFilter && !modeKeys.includes(modeFilter)) {
163
+ throw new TunnelServiceError('mode_unsupported', `Provider '${providerId}' does not support mode '${modeFilter}'`);
164
+ }
165
+
166
+ if (typeof provider.diagnose === 'function') {
167
+ const diagnosed = await provider.diagnose({
168
+ ...doctorRequest,
169
+ mode: modeFilter || doctorRequest?.mode,
170
+ }, {
171
+ capabilities,
172
+ });
173
+ const providerChecks = Array.isArray(diagnosed?.providerChecks) ? diagnosed.providerChecks : [];
174
+ const allModes = Array.isArray(diagnosed?.modes) ? diagnosed.modes : [];
175
+ const modes = modeFilter ? allModes.filter((entry) => entry?.mode === modeFilter) : allModes;
176
+ return {
177
+ ok: true,
178
+ provider: providerId,
179
+ providerChecks,
180
+ modes,
181
+ };
182
+ }
183
+
184
+ const availability = await tunnelService.checkAvailability(providerId);
185
+ const dependencyAvailable = Boolean(availability?.available);
186
+ const providerChecks = [{
187
+ id: 'dependency',
188
+ label: 'Provider dependency',
189
+ status: dependencyAvailable ? 'pass' : 'fail',
190
+ detail: dependencyAvailable
191
+ ? (availability?.version || 'available')
192
+ : (availability?.message || 'Required provider dependency is unavailable.'),
193
+ }];
194
+
195
+ const targetModes = (Array.isArray(capabilities.modes) ? capabilities.modes : [])
196
+ .filter((entry) => !modeFilter || entry?.key === modeFilter);
197
+ const modes = targetModes.map((entry) => createGenericModeChecks({
198
+ modeKey: entry.key,
199
+ requiredFields: Array.isArray(entry?.requires) ? entry.requires : [],
200
+ doctorRequest,
201
+ startupReady: dependencyAvailable,
202
+ }));
203
+
204
+ return {
205
+ ok: true,
206
+ provider: providerId,
207
+ providerChecks,
208
+ modes,
209
+ };
210
+ };
211
+
212
+ const registerRoutes = (app) => {
213
+ app.get('/api/vinci/tunnel/check', async (req, res) => {
214
+ try {
215
+ const requestedProvider = typeof req?.query?.provider === 'string' && req.query.provider.trim().length > 0
216
+ ? normalizeTunnelProvider(req.query.provider)
217
+ : await resolvePreferredTunnelProvider();
218
+ const result = await tunnelService.checkAvailability(requestedProvider);
219
+ res.json({
220
+ available: result.available,
221
+ provider: requestedProvider,
222
+ version: result.version || null,
223
+ });
224
+ } catch (error) {
225
+ console.warn('Tunnel dependency check failed:', error);
226
+ res.json({ available: false, provider: null, version: null });
227
+ }
228
+ });
229
+
230
+ const handleTunnelDoctor = async (req, res) => {
231
+ try {
232
+ const params = req.query || {};
233
+ const body = req.body || {};
234
+
235
+ const providerId = typeof params.provider === 'string' && params.provider.trim().length > 0
236
+ ? normalizeTunnelProvider(params.provider)
237
+ : await resolvePreferredTunnelProvider();
238
+ const modeFilter = typeof params.mode === 'string' && params.mode.trim().length > 0
239
+ ? params.mode.trim().toLowerCase()
240
+ : null;
241
+
242
+ const settings = await readSettingsFromDiskMigrated();
243
+ const selectedPresetId = typeof params.managedRemoteTunnelPresetId === 'string'
244
+ ? params.managedRemoteTunnelPresetId.trim()
245
+ : '';
246
+ const requestConfigPath = normalizeOptionalPath(params.configPath)
247
+ ?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
248
+ const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(params.managedRemoteTunnelHostname);
249
+ const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(params.tunnelHostname);
250
+ const requestHostname = normalizeManagedRemoteTunnelHostname(params.hostname);
251
+ const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
252
+ const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
253
+
254
+ const requestManagedRemoteToken = typeof body.managedRemoteTunnelToken === 'string'
255
+ ? body.managedRemoteTunnelToken.trim()
256
+ : '';
257
+ const requestTunnelToken = typeof body.tunnelToken === 'string'
258
+ ? body.tunnelToken.trim()
259
+ : '';
260
+ const requestToken = typeof body.token === 'string'
261
+ ? body.token.trim()
262
+ : '';
263
+ const requestTokenProvided = body.managedRemoteTunnelTokenProvided === true
264
+ || body.tunnelTokenProvided === true
265
+ || body.tokenProvided === true;
266
+ const requestHostnameProvided = body.managedRemoteTunnelHostnameProvided === true
267
+ || body.tunnelHostnameProvided === true
268
+ || body.hostnameProvided === true;
269
+ const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string'
270
+ ? settings.managedRemoteTunnelToken.trim()
271
+ : '';
272
+ const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
273
+ const serverHasSavedManagedRemoteProfile = managedRemoteTunnelConfig.tunnels.some((entry) => {
274
+ const savedHostname = normalizeManagedRemoteTunnelHostname(entry?.hostname);
275
+ const savedToken = typeof entry?.token === 'string' ? entry.token.trim() : '';
276
+ return Boolean(savedHostname && savedToken);
277
+ });
278
+ const cliHasSavedManagedRemoteProfile = params.hasSavedManagedRemoteProfile === '1';
279
+ const hasSavedManagedRemoteProfile = serverHasSavedManagedRemoteProfile || cliHasSavedManagedRemoteProfile;
280
+ const configManagedRemoteToken = providerId === TUNNEL_PROVIDER_CLOUDFLARE
281
+ ? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
282
+ : '';
283
+ const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
284
+ const runtimeToken = getRuntimeManagedRemoteTunnelToken();
285
+ const token = requestToken
286
+ || requestTunnelToken
287
+ || requestManagedRemoteToken
288
+ || ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
289
+ || configManagedRemoteToken
290
+ || storedManagedRemoteToken;
291
+
292
+ const doctorRequest = {
293
+ mode: modeFilter,
294
+ hostname,
295
+ token,
296
+ tokenProvided: requestTokenProvided,
297
+ hostnameProvided: requestHostnameProvided,
298
+ configPath: requestConfigPath,
299
+ hasSavedManagedRemoteProfile,
300
+ };
301
+
302
+ const result = await runTunnelDoctor({
303
+ providerId,
304
+ modeFilter,
305
+ doctorRequest,
306
+ });
307
+ return res.json(result);
308
+ } catch (error) {
309
+ if (error instanceof TunnelServiceError) {
310
+ return res.status(400).json({ ok: false, error: error.message, code: error.code });
311
+ }
312
+ console.warn('Tunnel doctor failed:', error);
313
+ return res.status(500).json({ ok: false, error: 'Failed to run tunnel doctor' });
314
+ }
315
+ };
316
+ app.post('/api/vinci/tunnel/doctor', handleTunnelDoctor);
317
+ app.get('/api/vinci/tunnel/doctor', handleTunnelDoctor);
318
+
319
+ app.get('/api/vinci/tunnel/providers', (_req, res) => {
320
+ const providers = tunnelProviderRegistry.listCapabilities();
321
+ return res.json({ providers });
322
+ });
323
+
324
+ app.get('/api/vinci/tunnel/status', async (_req, res) => {
325
+ try {
326
+ const settings = await readSettingsFromDiskMigrated();
327
+ const normalizedMode = normalizeTunnelMode(settings?.tunnelMode);
328
+ const managedRemoteHostname = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
329
+ const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
330
+ const managedRemoteTunnelPresetSummaries = managedRemoteTunnelConfig.tunnels.map((entry) => ({
331
+ id: entry.id,
332
+ name: entry.name,
333
+ hostname: entry.hostname,
334
+ }));
335
+ const hasStoredManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
336
+ const hasManagedRemoteTunnelToken = getRuntimeManagedRemoteTunnelToken().length > 0 || managedRemoteTunnelConfig.tunnels.length > 0 || hasStoredManagedRemoteToken;
337
+ const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null
338
+ ? null
339
+ : normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs);
340
+ const sessionTtlMs = normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
341
+ const activeSessions = tunnelAuthController.listTunnelSessions();
342
+ const activeProvider = tunnelService.resolveActiveProvider();
343
+ const provider = activeProvider || normalizeTunnelProvider(settings?.tunnelProvider);
344
+
345
+ const publicUrl = tunnelService.getPublicUrl();
346
+ if (!publicUrl) {
347
+ return res.json({
348
+ active: false,
349
+ url: null,
350
+ mode: normalizedMode,
351
+ provider,
352
+ providerMetadata: null,
353
+ hasManagedRemoteTunnelToken,
354
+ managedRemoteTunnelHostname: managedRemoteHostname || null,
355
+ managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
356
+ managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
357
+ hasBootstrapToken: false,
358
+ bootstrapExpiresAt: null,
359
+ policy: 'tunnel-gated',
360
+ activeTunnelMode: tunnelAuthController.getActiveTunnelMode() || null,
361
+ activeSessions,
362
+ localPort: getActivePort(),
363
+ ttlConfig: {
364
+ bootstrapTtlMs,
365
+ sessionTtlMs,
366
+ },
367
+ });
368
+ }
369
+
370
+ const activeNormalizedMode = resolveActiveNormalizedTunnelMode();
371
+ const activeTunnelId = tunnelAuthController.getActiveTunnelId();
372
+ const activeTunnelHost = tunnelAuthController.getActiveTunnelHost();
373
+ const resolvedTunnelHost = resolveNormalizedTunnelHost(publicUrl);
374
+ const activeTunnelMode = tunnelAuthController.getActiveTunnelMode();
375
+ const needsActiveTunnelSync = !activeTunnelId
376
+ || !activeTunnelHost
377
+ || !resolvedTunnelHost
378
+ || activeTunnelHost !== resolvedTunnelHost
379
+ || activeTunnelMode !== activeNormalizedMode;
380
+ if (needsActiveTunnelSync) {
381
+ tunnelAuthController.setActiveTunnel({
382
+ tunnelId: activeTunnelId || crypto.randomUUID(),
383
+ publicUrl,
384
+ mode: activeNormalizedMode,
385
+ });
386
+ }
387
+
388
+ const bootstrapStatus = tunnelAuthController.getBootstrapStatus();
389
+ const providerMetadata = tunnelService.getProviderMetadata();
390
+
391
+ return res.json({
392
+ active: true,
393
+ url: publicUrl,
394
+ mode: activeNormalizedMode,
395
+ provider,
396
+ providerMetadata,
397
+ hasManagedRemoteTunnelToken,
398
+ managedRemoteTunnelHostname: managedRemoteHostname || null,
399
+ managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
400
+ managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
401
+ hasBootstrapToken: bootstrapStatus.hasBootstrapToken,
402
+ bootstrapExpiresAt: bootstrapStatus.bootstrapExpiresAt,
403
+ policy: 'tunnel-gated',
404
+ activeTunnelMode: activeNormalizedMode,
405
+ activeSessions: tunnelAuthController.listTunnelSessions(),
406
+ localPort: getActivePort(),
407
+ ttlConfig: {
408
+ bootstrapTtlMs,
409
+ sessionTtlMs,
410
+ },
411
+ });
412
+ } catch (error) {
413
+ return res.status(500).json({ error: 'Failed to get tunnel status' });
414
+ }
415
+ });
416
+
417
+ app.put('/api/vinci/tunnel/managed-remote-token', async (req, res) => {
418
+ try {
419
+ const presetId = typeof req?.body?.presetId === 'string' ? req.body.presetId.trim() : '';
420
+ const presetName = typeof req?.body?.presetName === 'string' ? req.body.presetName.trim() : '';
421
+ const managedRemoteTunnelHostname = normalizeManagedRemoteTunnelHostname(req?.body?.managedRemoteTunnelHostname);
422
+ const managedRemoteTunnelToken = typeof req?.body?.managedRemoteTunnelToken === 'string' ? req.body.managedRemoteTunnelToken.trim() : '';
423
+
424
+ if (!presetId || !presetName || !managedRemoteTunnelHostname || !managedRemoteTunnelToken) {
425
+ return res.status(400).json({ ok: false, error: 'presetId, presetName, managedRemoteTunnelHostname and managedRemoteTunnelToken are required' });
426
+ }
427
+
428
+ await upsertManagedRemoteTunnelToken({
429
+ id: presetId,
430
+ name: presetName,
431
+ hostname: managedRemoteTunnelHostname,
432
+ token: managedRemoteTunnelToken,
433
+ });
434
+
435
+ const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
436
+ return res.json({ ok: true, managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) });
437
+ } catch (error) {
438
+ return res.status(500).json({ ok: false, error: 'Failed to save managed remote tunnel token' });
439
+ }
440
+ });
441
+
442
+ app.post('/api/vinci/tunnel/start', async (_req, res) => {
443
+ try {
444
+ const settings = await readSettingsFromDiskMigrated();
445
+ if (typeof _req?.body?.provider === 'string' && _req.body.provider.trim().length > 0) {
446
+ const rawProvider = _req.body.provider.trim().toLowerCase();
447
+ if (!tunnelProviderRegistry.get(rawProvider)) {
448
+ return res.status(422).json({ ok: false, error: `Unsupported tunnel provider: ${rawProvider}`, code: 'provider_unsupported' });
449
+ }
450
+ }
451
+ const provider = normalizeTunnelProvider(_req?.body?.provider ?? settings?.tunnelProvider);
452
+ const modeInput = _req?.body?.mode ?? settings?.tunnelMode;
453
+ const intent = typeof _req?.body?.intent === 'string' ? _req.body.intent.trim().toLowerCase() : undefined;
454
+ const mode = typeof modeInput === 'string'
455
+ ? modeInput.trim().toLowerCase()
456
+ : normalizeTunnelMode(modeInput);
457
+ if (typeof _req?.body?.mode === 'string' && _req.body.mode.trim().length > 0 && !isSupportedTunnelMode(mode)) {
458
+ return res.status(422).json({ ok: false, error: `Unsupported tunnel mode: ${mode}`, code: 'mode_unsupported' });
459
+ }
460
+ const selectedPresetId = typeof _req?.body?.managedRemoteTunnelPresetId === 'string' ? _req.body.managedRemoteTunnelPresetId.trim() : '';
461
+ const selectedPresetName = typeof _req?.body?.managedRemoteTunnelPresetName === 'string' ? _req.body.managedRemoteTunnelPresetName.trim() : '';
462
+ const requestConfigPath = normalizeOptionalPath(_req?.body?.configPath)
463
+ ?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
464
+ const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.managedRemoteTunnelHostname);
465
+ const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.tunnelHostname);
466
+ const requestHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.hostname);
467
+ const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
468
+ const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
469
+ const requestManagedRemoteToken = typeof _req?.body?.managedRemoteTunnelToken === 'string' ? _req.body.managedRemoteTunnelToken.trim() : '';
470
+ const requestTunnelToken = typeof _req?.body?.tunnelToken === 'string' ? _req.body.tunnelToken.trim() : '';
471
+ const requestToken = typeof _req?.body?.token === 'string' ? _req.body.token.trim() : '';
472
+ const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' ? settings.managedRemoteTunnelToken.trim() : '';
473
+ const configManagedRemoteToken = provider === TUNNEL_PROVIDER_CLOUDFLARE
474
+ ? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
475
+ : '';
476
+ const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
477
+ const runtimeToken = getRuntimeManagedRemoteTunnelToken();
478
+ const token = requestToken
479
+ || requestTunnelToken
480
+ || requestManagedRemoteToken
481
+ || ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
482
+ || configManagedRemoteToken
483
+ || storedManagedRemoteToken;
484
+ const requestConnectTtlMs = typeof _req?.body?.connectTtlMs === 'number' && Number.isFinite(_req.body.connectTtlMs)
485
+ ? normalizeTunnelBootstrapTtlMs(_req.body.connectTtlMs)
486
+ : undefined;
487
+ const requestSessionTtlMs = typeof _req?.body?.sessionTtlMs === 'number' && Number.isFinite(_req.body.sessionTtlMs)
488
+ ? normalizeTunnelSessionTtlMs(_req.body.sessionTtlMs)
489
+ : undefined;
490
+ const bootstrapTtlMs = requestConnectTtlMs ?? (settings?.tunnelBootstrapTtlMs === null
491
+ ? null
492
+ : normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs));
493
+ const sessionTtlMs = requestSessionTtlMs ?? normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
494
+
495
+ const previousTunnelId = tunnelAuthController.getActiveTunnelId();
496
+ const previousMode = tunnelAuthController.getActiveTunnelMode();
497
+ const previousProvider = tunnelService.resolveActiveProvider();
498
+ const previousUrl = tunnelService.getPublicUrl();
499
+
500
+ const { publicUrl, provider: activeProvider, providerMetadata } = await startTunnelWithNormalizedRequest({
501
+ provider,
502
+ mode,
503
+ intent,
504
+ hostname,
505
+ token,
506
+ configPath: requestConfigPath,
507
+ selectedPresetId,
508
+ selectedPresetName,
509
+ });
510
+
511
+ const replacedTunnel = Boolean(previousTunnelId) && (
512
+ previousMode !== mode
513
+ || previousProvider !== activeProvider
514
+ || previousUrl !== publicUrl
515
+ );
516
+ let revokedBootstrapCount = 0;
517
+ let invalidatedSessionCount = 0;
518
+ if (replacedTunnel && previousTunnelId) {
519
+ const revoked = tunnelAuthController.revokeTunnelArtifacts(previousTunnelId);
520
+ revokedBootstrapCount = revoked.revokedBootstrapCount;
521
+ invalidatedSessionCount = revoked.invalidatedSessionCount;
522
+ }
523
+
524
+ tunnelAuthController.setActiveTunnel({
525
+ tunnelId: replacedTunnel || !previousTunnelId ? crypto.randomUUID() : previousTunnelId,
526
+ publicUrl,
527
+ mode,
528
+ });
529
+
530
+ const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs });
531
+ const connectUrl = `${publicUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`;
532
+ const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
533
+ const isCloudflareProvider = activeProvider === TUNNEL_PROVIDER_CLOUDFLARE;
534
+
535
+ return res.json({
536
+ ok: true,
537
+ url: publicUrl,
538
+ mode,
539
+ provider: activeProvider,
540
+ providerMetadata,
541
+ managedRemoteTunnelHostname: isCloudflareProvider ? (hostname || null) : null,
542
+ managedRemoteTunnelTokenPresetIds: isCloudflareProvider ? managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) : [],
543
+ connectUrl,
544
+ bootstrapExpiresAt: bootstrapToken.expiresAt,
545
+ replacedTunnel,
546
+ replaced: replacedTunnel
547
+ ? {
548
+ mode: previousMode,
549
+ provider: previousProvider,
550
+ url: previousUrl,
551
+ }
552
+ : null,
553
+ revokedBootstrapCount,
554
+ invalidatedSessionCount,
555
+ policy: 'tunnel-gated',
556
+ activeTunnelMode: mode,
557
+ activeSessions: tunnelAuthController.listTunnelSessions(),
558
+ localPort: getActivePort(),
559
+ ttlConfig: {
560
+ bootstrapTtlMs,
561
+ sessionTtlMs,
562
+ },
563
+ });
564
+ } catch (error) {
565
+ console.error('Failed to start tunnel:', error);
566
+ setActiveTunnelController(null);
567
+ tunnelAuthController.clearActiveTunnel();
568
+ if (error instanceof TunnelServiceError) {
569
+ const status = error.code === 'missing_dependency'
570
+ ? 400
571
+ : (error.code === 'validation_error' || error.code === 'provider_unsupported' || error.code === 'mode_unsupported'
572
+ ? 422
573
+ : 500);
574
+ return res.status(status).json({ ok: false, error: error.message, code: error.code });
575
+ }
576
+ return res.status(500).json({ ok: false, error: 'Failed to start tunnel', code: 'startup_failed' });
577
+ }
578
+ });
579
+
580
+ app.post('/api/vinci/tunnel/stop', (_req, res) => {
581
+ let revokedBootstrapCount = 0;
582
+ let invalidatedSessionCount = 0;
583
+ const activeTunnelId = tunnelAuthController.getActiveTunnelId();
584
+
585
+ if (activeTunnelId) {
586
+ const revoked = tunnelAuthController.revokeTunnelArtifacts(activeTunnelId);
587
+ revokedBootstrapCount = revoked.revokedBootstrapCount;
588
+ invalidatedSessionCount = revoked.invalidatedSessionCount;
589
+ }
590
+
591
+ if (getActiveTunnelController()) {
592
+ console.log('Stopping active tunnel (user requested)...');
593
+ tunnelService.stop();
594
+ }
595
+
596
+ tunnelAuthController.clearActiveTunnel();
597
+ res.json({ ok: true, revokedBootstrapCount, invalidatedSessionCount });
598
+ });
599
+ };
600
+
601
+ return {
602
+ registerRoutes,
603
+ startTunnelWithNormalizedRequest,
604
+ };
605
+ };