@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,1333 @@
1
+ const DEFAULT_TARGET_TTL_MS = 30 * 60 * 1000;
2
+ const TOKEN_COOKIE_NAME = 'oc_preview_token';
3
+
4
+ const LOOPBACK_HOSTS = new Set([
5
+ 'localhost',
6
+ '127.0.0.1',
7
+ '::1',
8
+ '[::1]',
9
+ '0.0.0.0',
10
+ ]);
11
+
12
+ const PREVIEW_BRIDGE_SCRIPT_ID = 'vinci-preview-bridge';
13
+
14
+ const parsePreviewResourcePath = (url) => {
15
+ try {
16
+ const parsed = new URL(String(url || ''), 'http://localhost');
17
+ const match = parsed.pathname.match(/^\/api\/preview\/proxy\/[a-f0-9]{16,64}(\/.*)?$/i);
18
+ const path = match ? (match[1] || '/') : parsed.pathname;
19
+ return path + parsed.search;
20
+ } catch {
21
+ return String(url || '');
22
+ }
23
+ };
24
+
25
+ const previewResourceNoiseRuleSets = [
26
+ {
27
+ name: 'vite',
28
+ suppress: ({ lower, path, tag }) => path === '/@vite/client'
29
+ || path === '/@react-refresh'
30
+ || path.startsWith('/@id/__x00__vite/')
31
+ || lower.includes('/node_modules/.vite/')
32
+ || lower.includes('/vite/dist/client/')
33
+ || (tag === 'script' && lower.includes('/@id/')),
34
+ },
35
+ {
36
+ name: 'astro',
37
+ suppress: ({ lower, path, tag }) => path.startsWith('/@id/astro:')
38
+ || lower.includes('/astro/dist/runtime/client/dev-toolbar/')
39
+ || (tag === 'script' && lower.includes('.astro?') && lower.includes('type=script'))
40
+ || (tag === 'script' && (
41
+ lower.endsWith('.css')
42
+ || lower.includes('.css?')
43
+ || lower.includes('type=style')
44
+ || lower.includes('lang.css')
45
+ )),
46
+ },
47
+ {
48
+ name: 'next',
49
+ suppress: ({ lower, path, tag }) => tag === 'script' && (
50
+ path === '/_next/webpack-hmr'
51
+ || lower.includes('/_next/static/webpack/')
52
+ || lower.includes('/_next/static/chunks/webpack')
53
+ || lower.includes('/_next/static/chunks/react-refresh')
54
+ || lower.includes('/_next/static/development/')
55
+ ),
56
+ },
57
+ {
58
+ name: 'sveltekit',
59
+ suppress: ({ lower, tag }) => tag === 'script' && (
60
+ lower.includes('/@id/__x00__virtual:')
61
+ || lower.includes('/@id/virtual:')
62
+ || lower.includes('/.svelte-kit/generated/')
63
+ || lower.includes('/node_modules/.vite/deps/')
64
+ ),
65
+ },
66
+ {
67
+ name: 'remix',
68
+ suppress: ({ lower, tag }) => tag === 'script' && (
69
+ lower.includes('/@remix-run/dev/')
70
+ || lower.includes('/__manifest')
71
+ || lower.includes('/__hmr')
72
+ ),
73
+ },
74
+ {
75
+ name: 'nuxt',
76
+ suppress: ({ lower, tag }) => tag === 'script' && (
77
+ lower.includes('/_nuxt/@vite/client')
78
+ || lower.includes('/_nuxt/@id/')
79
+ || lower.includes('/_nuxt/node_modules/.vite/')
80
+ || lower.includes('/__nuxt_error')
81
+ || lower.includes('/__nuxt_vite_node__')
82
+ ),
83
+ },
84
+ {
85
+ name: 'webpack',
86
+ suppress: ({ lower, path, tag }) => tag === 'script' && (
87
+ path === '/sockjs-node/info'
88
+ || lower.includes('/webpack-dev-server/')
89
+ || lower.includes('/webpack/hot/')
90
+ || lower.includes('/__webpack_hmr')
91
+ || lower.includes('/ws') && lower.includes('webpack')
92
+ ),
93
+ },
94
+ ];
95
+
96
+ export const classifyPreviewResourceError = ({ tagName, url }) => {
97
+ const tag = typeof tagName === 'string' ? tagName.toLowerCase() : '';
98
+ if (tag !== 'script' && tag !== 'link') return 'report';
99
+
100
+ const pathAndSearch = parsePreviewResourcePath(url);
101
+ const lower = pathAndSearch.toLowerCase();
102
+ const path = pathAndSearch.split('?', 1)[0] || '';
103
+ const context = { tag, path, pathAndSearch, lower };
104
+
105
+ if (previewResourceNoiseRuleSets.some((ruleSet) => ruleSet.suppress(context))) return 'suppress';
106
+
107
+ return 'report';
108
+ };
109
+
110
+ export const classifyPreviewNavigation = ({ url, currentUrl }) => {
111
+ let parsed;
112
+ try {
113
+ parsed = new URL(String(url || ''), currentUrl || 'http://localhost/');
114
+ } catch {
115
+ return { action: 'allow', url: String(url || '') };
116
+ }
117
+
118
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
119
+ return { action: 'allow', url: parsed.toString() };
120
+ }
121
+
122
+ let current;
123
+ try {
124
+ current = new URL(currentUrl || 'http://localhost/');
125
+ } catch {
126
+ current = null;
127
+ }
128
+
129
+ if (current
130
+ && parsed.origin === current.origin
131
+ && parsed.pathname === current.pathname
132
+ && parsed.search === current.search
133
+ && parsed.hash
134
+ ) {
135
+ return { action: 'allow', url: parsed.toString() };
136
+ }
137
+
138
+ const path = parsed.pathname || '/';
139
+ if (parsed.origin === current?.origin && path.startsWith('/api/preview/proxy/')) {
140
+ return { action: 'allow', url: parsed.toString() };
141
+ }
142
+
143
+ const host = parsed.hostname;
144
+ const isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1' || host === '[::1]';
145
+ if (isLoopback || (parsed.origin === current?.origin && path.startsWith('/'))) {
146
+ return { action: 'proxy', url: parsed.toString() };
147
+ }
148
+
149
+ return { action: 'external', url: parsed.toString() };
150
+ };
151
+
152
+ const PREVIEW_BRIDGE_SCRIPT = String.raw`(() => {
153
+ if (window.__vinciPreviewBridgeInstalled) return;
154
+ window.__vinciPreviewBridgeInstalled = true;
155
+
156
+ const SOURCE = 'vinci-preview-bridge';
157
+ const VERSION = 1;
158
+ const MAX_TEXT = 500;
159
+ const MAX_ARG = 1000;
160
+ let inspectMode = false;
161
+ let lastHoverKey = '';
162
+ let pendingHover = null;
163
+ let previewColorScheme = null;
164
+ let nativeMatchMedia = null;
165
+ const colorSchemeListeners = new Set();
166
+
167
+ const post = (payload) => {
168
+ try {
169
+ if (window.parent && typeof window.parent.postMessage === 'function') {
170
+ const message = Object.assign({ source: SOURCE, version: VERSION }, payload || {});
171
+ window.parent.postMessage(message, window.location.origin);
172
+ }
173
+ } catch {}
174
+ };
175
+
176
+ const clip = (value, max = MAX_TEXT) => {
177
+ const text = String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
178
+ return text.length > max ? text.slice(0, max) + '...' : text;
179
+ };
180
+
181
+ const stringifyArg = (value) => {
182
+ if (typeof value === 'string') return clip(value, MAX_ARG);
183
+ if (value instanceof Error) return clip(value.stack || value.message || String(value), MAX_ARG);
184
+ try {
185
+ return clip(JSON.stringify(value), MAX_ARG);
186
+ } catch {
187
+ return clip(String(value), MAX_ARG);
188
+ }
189
+ };
190
+
191
+ const normalizeColorScheme = (value) => value === 'dark' ? 'dark' : value === 'light' ? 'light' : null;
192
+
193
+ const mediaQueryColorScheme = (query) => {
194
+ const normalized = String(query || '').replace(/\s+/g, ' ').trim().toLowerCase();
195
+ if (normalized === '(prefers-color-scheme: dark)') return 'dark';
196
+ if (normalized === '(prefers-color-scheme: light)') return 'light';
197
+ return null;
198
+ };
199
+
200
+ const mediaQueryMatchesPreviewScheme = (query) => {
201
+ const scheme = mediaQueryColorScheme(query);
202
+ if (!scheme || !previewColorScheme) return null;
203
+ return previewColorScheme === scheme;
204
+ };
205
+
206
+ const notifyColorSchemeListeners = () => {
207
+ for (const listener of Array.from(colorSchemeListeners)) {
208
+ try {
209
+ const matches = mediaQueryMatchesPreviewScheme(listener.media);
210
+ if (matches === null) continue;
211
+ const event = { matches, media: listener.media, type: 'change', target: listener.mql, currentTarget: listener.mql };
212
+ listener.callback.call(listener.mql, event);
213
+ } catch {}
214
+ }
215
+ };
216
+
217
+ const installColorSchemeMatchMediaPatch = () => {
218
+ if (window.__vinciPreviewColorSchemePatched || typeof window.matchMedia !== 'function') return;
219
+ window.__vinciPreviewColorSchemePatched = true;
220
+ nativeMatchMedia = window.matchMedia.bind(window);
221
+ window.matchMedia = function(query) {
222
+ const nativeMql = nativeMatchMedia(query);
223
+ if (!mediaQueryColorScheme(query)) return nativeMql;
224
+ const listenersForMql = new Map();
225
+ const mql = Object.create(nativeMql);
226
+ Object.defineProperty(mql, 'matches', { get: () => mediaQueryMatchesPreviewScheme(query) ?? nativeMql.matches });
227
+ Object.defineProperty(mql, 'media', { get: () => nativeMql.media });
228
+ mql.addEventListener = function(type, callback, options) {
229
+ if (type !== 'change' || typeof callback !== 'function') return nativeMql.addEventListener?.(type, callback, options);
230
+ const entry = { media: query, mql, callback };
231
+ listenersForMql.set(callback, entry);
232
+ colorSchemeListeners.add(entry);
233
+ };
234
+ mql.removeEventListener = function(type, callback, options) {
235
+ if (type !== 'change' || typeof callback !== 'function') return nativeMql.removeEventListener?.(type, callback, options);
236
+ const entry = listenersForMql.get(callback);
237
+ if (entry) colorSchemeListeners.delete(entry);
238
+ listenersForMql.delete(callback);
239
+ };
240
+ mql.addListener = function(callback) { mql.addEventListener('change', callback); };
241
+ mql.removeListener = function(callback) { mql.removeEventListener('change', callback); };
242
+ return mql;
243
+ };
244
+ };
245
+
246
+ const shouldSyncDataTheme = () => {
247
+ try {
248
+ const root = document.documentElement;
249
+ if (!root) return false;
250
+ if (root.hasAttribute('data-theme')) return true;
251
+ if (document.querySelector('starlight-theme-select, starlight-menu-button')) return true;
252
+ const generator = document.querySelector('meta[name="generator"]');
253
+ const generatorContent = generator && typeof generator.getAttribute === 'function' ? generator.getAttribute('content') || '' : '';
254
+ if (generatorContent.toLowerCase().indexOf('starlight') >= 0) return true;
255
+ const styles = window.getComputedStyle(root);
256
+ return Boolean(styles.getPropertyValue('--sl-color-bg').trim()
257
+ || styles.getPropertyValue('--sl-color-text').trim()
258
+ || styles.getPropertyValue('--sl-color-accent').trim());
259
+ } catch {
260
+ return false;
261
+ }
262
+ };
263
+
264
+ const applyPreviewColorScheme = (scheme) => {
265
+ const next = normalizeColorScheme(scheme);
266
+ if (!next || previewColorScheme === next) return;
267
+ previewColorScheme = next;
268
+ try {
269
+ const root = document.documentElement;
270
+ root.style.colorScheme = next;
271
+ root.dataset.vinciPreviewColorScheme = next;
272
+ if (shouldSyncDataTheme()) {
273
+ root.dataset.theme = next;
274
+ }
275
+ } catch {}
276
+ notifyColorSchemeListeners();
277
+ };
278
+
279
+ const readElementUrl = (element) => {
280
+ return element.currentSrc || element.src || element.href || element.action || '';
281
+ };
282
+
283
+ const upstreamPathForUrl = (value) => {
284
+ try {
285
+ const parsed = new URL(value, window.location.href);
286
+ const match = parsed.pathname.match(/^\/api\/preview\/proxy\/[a-f0-9]{16,64}(\/.*)?$/i);
287
+ return match ? (match[1] || '/') : parsed.pathname;
288
+ } catch {
289
+ return String(value || '');
290
+ }
291
+ };
292
+
293
+ const upstreamPathAndSearchForUrl = (value) => {
294
+ try {
295
+ const parsed = new URL(value, window.location.href);
296
+ const match = parsed.pathname.match(/^\/api\/preview\/proxy\/[a-f0-9]{16,64}(\/.*)?$/i);
297
+ const path = match ? (match[1] || '/') : parsed.pathname;
298
+ return path + parsed.search;
299
+ } catch {
300
+ return String(value || '');
301
+ }
302
+ };
303
+
304
+ const isInternalDevToolResource = (element, value) => {
305
+ const tag = element && element.tagName && typeof element.tagName.toLowerCase === 'function' ? element.tagName.toLowerCase() : '';
306
+ if (tag !== 'script' && tag !== 'link') return false;
307
+ if (tag === 'script' && typeof element.hasAttribute === 'function' && element.hasAttribute('data-cf-beacon')) return true;
308
+ const pathAndSearch = upstreamPathAndSearchForUrl(value);
309
+ const lower = pathAndSearch.toLowerCase();
310
+ const path = pathAndSearch.split('?', 1)[0] || '';
311
+
312
+ const viteNoise = path === '/@vite/client'
313
+ || path === '/@react-refresh'
314
+ || path.indexOf('/@id/__x00__vite/') === 0
315
+ || lower.indexOf('/node_modules/.vite/') >= 0
316
+ || lower.indexOf('/vite/dist/client/') >= 0
317
+ || (tag === 'script' && lower.indexOf('/@id/') >= 0);
318
+ const astroNoise = path.indexOf('/@id/astro:') === 0
319
+ || lower.indexOf('/astro/dist/runtime/client/dev-toolbar/') >= 0
320
+ || (tag === 'script' && lower.indexOf('.astro?') >= 0 && lower.indexOf('type=script') >= 0)
321
+ || (tag === 'script' && (
322
+ lower.endsWith('.css')
323
+ || lower.indexOf('.css?') >= 0
324
+ || lower.indexOf('type=style') >= 0
325
+ || lower.indexOf('lang.css') >= 0
326
+ ));
327
+ const nextNoise = tag === 'script' && (
328
+ path === '/_next/webpack-hmr'
329
+ || lower.indexOf('/_next/static/webpack/') >= 0
330
+ || lower.indexOf('/_next/static/chunks/webpack') >= 0
331
+ || lower.indexOf('/_next/static/chunks/react-refresh') >= 0
332
+ || lower.indexOf('/_next/static/development/') >= 0
333
+ );
334
+ const svelteKitNoise = tag === 'script' && (
335
+ lower.indexOf('/@id/__x00__virtual:') >= 0
336
+ || lower.indexOf('/@id/virtual:') >= 0
337
+ || lower.indexOf('/.svelte-kit/generated/') >= 0
338
+ || lower.indexOf('/node_modules/.vite/deps/') >= 0
339
+ );
340
+ const remixNoise = tag === 'script' && (
341
+ lower.indexOf('/@remix-run/dev/') >= 0
342
+ || lower.indexOf('/__manifest') >= 0
343
+ || lower.indexOf('/__hmr') >= 0
344
+ );
345
+ const nuxtNoise = tag === 'script' && (
346
+ lower.indexOf('/_nuxt/@vite/client') >= 0
347
+ || lower.indexOf('/_nuxt/@id/') >= 0
348
+ || lower.indexOf('/_nuxt/node_modules/.vite/') >= 0
349
+ || lower.indexOf('/__nuxt_error') >= 0
350
+ || lower.indexOf('/__nuxt_vite_node__') >= 0
351
+ );
352
+ const webpackNoise = tag === 'script' && (
353
+ path === '/sockjs-node/info'
354
+ || lower.indexOf('/webpack-dev-server/') >= 0
355
+ || lower.indexOf('/webpack/hot/') >= 0
356
+ || lower.indexOf('/__webpack_hmr') >= 0
357
+ || (lower.indexOf('/ws') >= 0 && lower.indexOf('webpack') >= 0)
358
+ );
359
+
360
+ if (viteNoise || astroNoise || nextNoise || svelteKitNoise || remixNoise || nuxtNoise || webpackNoise) return true;
361
+ return false;
362
+ };
363
+
364
+ installColorSchemeMatchMediaPatch();
365
+
366
+ const classifyNavigation = (value) => {
367
+ try {
368
+ const parsed = new URL(value, window.location.href);
369
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return { action: 'allow', url: parsed.toString() };
370
+ const current = new URL(window.location.href);
371
+ if (parsed.origin === current.origin && parsed.pathname === current.pathname && parsed.search === current.search && parsed.hash) {
372
+ return { action: 'allow', url: parsed.toString() };
373
+ }
374
+ if (parsed.origin === current.origin && parsed.pathname.startsWith('/api/preview/proxy/')) {
375
+ return { action: 'allow', url: parsed.toString() };
376
+ }
377
+ const host = parsed.hostname;
378
+ const isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1' || host === '[::1]';
379
+ if (isLoopback || (parsed.origin === current.origin && parsed.pathname.startsWith('/'))) {
380
+ return { action: 'proxy', url: parsed.toString() };
381
+ }
382
+ return { action: 'external', url: parsed.toString() };
383
+ } catch {
384
+ return { action: 'allow', url: String(value || '') };
385
+ }
386
+ };
387
+
388
+ const isInternalDevToolRuntimeError = (filename) => {
389
+ const path = upstreamPathForUrl(filename || '');
390
+ const pathAndSearch = upstreamPathAndSearchForUrl(filename || '');
391
+ const lowerPathAndSearch = pathAndSearch.toLowerCase();
392
+ const isStyleRuntimeNoise = lowerPathAndSearch.endsWith('.css')
393
+ || lowerPathAndSearch.indexOf('.css?') >= 0
394
+ || lowerPathAndSearch.indexOf('type=style') >= 0
395
+ || lowerPathAndSearch.indexOf('lang.css') >= 0;
396
+ return path === '/@vite/client'
397
+ || path === '/@react-refresh'
398
+ || path.indexOf('/astro/dist/runtime/client/dev-toolbar/') >= 0
399
+ || path.indexOf('/node_modules/.vite/') >= 0
400
+ || isStyleRuntimeNoise;
401
+ };
402
+
403
+ const isInternalDevToolConsoleNoise = (level, args) => {
404
+ if (level !== 'error' || typeof args[0] !== 'string' || args[0].indexOf('[vite]') !== 0) return false;
405
+ const text = args.map((arg) => stringifyArg(arg)).join(' ');
406
+ return text.indexOf('failed to connect to websocket') >= 0
407
+ || text.indexOf("Cannot read properties of undefined (reading 'send')") >= 0
408
+ || text.indexOf('Cannot read properties of undefined (reading "send")') >= 0;
409
+ };
410
+
411
+ const installViteHmrProxyPatch = () => {
412
+ if (window.__vinciViteHmrProxyPatched || typeof window.WebSocket !== 'function') return;
413
+ window.__vinciViteHmrProxyPatched = true;
414
+ const NativeWebSocket = window.WebSocket;
415
+ const proxyMatch = window.location.pathname.match(/^(\/api\/preview\/proxy\/[a-f0-9]{16,64})(?:\/|$)/i);
416
+ if (!proxyMatch) return;
417
+ const proxyBase = proxyMatch[1] + '/';
418
+ let reloadTimer = 0;
419
+
420
+ const schedulePreviewReload = () => {
421
+ if (reloadTimer) return;
422
+ reloadTimer = window.setTimeout(() => {
423
+ reloadTimer = 0;
424
+ try {
425
+ window.location.reload();
426
+ } catch {}
427
+ }, 80);
428
+ };
429
+
430
+ const rewriteUrl = (url, protocols) => {
431
+ const protocolList = Array.isArray(protocols) ? protocols : [protocols];
432
+ const isViteSocket = protocolList.indexOf('vite-hmr') >= 0 || protocolList.indexOf('vite-ping') >= 0;
433
+ if (!isViteSocket) return url;
434
+ try {
435
+ const parsed = new URL(String(url), window.location.href);
436
+ if (parsed.host !== window.location.host) return url;
437
+ if (parsed.pathname.indexOf(proxyBase) === 0) return url;
438
+ parsed.pathname = proxyBase;
439
+ return parsed.toString();
440
+ } catch {
441
+ return url;
442
+ }
443
+ };
444
+
445
+ function VinciPreviewWebSocket(url, protocols) {
446
+ const protocolList = Array.isArray(protocols) ? protocols : [protocols];
447
+ const isViteSocket = protocolList.indexOf('vite-hmr') >= 0;
448
+ const nextUrl = rewriteUrl(url, protocols);
449
+ const socket = arguments.length === 1
450
+ ? new NativeWebSocket(nextUrl)
451
+ : new NativeWebSocket(nextUrl, protocols);
452
+
453
+ if (isViteSocket) {
454
+ socket.addEventListener('message', (event) => {
455
+ try {
456
+ const payload = JSON.parse(String(event.data || ''));
457
+ if (payload && (payload.type === 'update' || payload.type === 'full-reload')) {
458
+ schedulePreviewReload();
459
+ }
460
+ } catch {}
461
+ });
462
+ }
463
+
464
+ return socket;
465
+ }
466
+
467
+ VinciPreviewWebSocket.prototype = NativeWebSocket.prototype;
468
+ Object.setPrototypeOf(VinciPreviewWebSocket, NativeWebSocket);
469
+ Object.defineProperty(VinciPreviewWebSocket, 'name', { value: 'WebSocket' });
470
+ window.WebSocket = VinciPreviewWebSocket;
471
+ };
472
+
473
+ const installAppRequestProxyPatch = () => {
474
+ if (window.__vinciAppRequestProxyPatched) return;
475
+ window.__vinciAppRequestProxyPatched = true;
476
+ const proxyMatch = window.location.pathname.match(/^(\/api\/preview\/proxy\/[a-f0-9]{16,64})(?:\/|$)/i);
477
+ if (!proxyMatch) return;
478
+ const proxyBase = proxyMatch[1];
479
+
480
+ const shouldProxyPath = (pathname) => {
481
+ if (typeof pathname !== 'string' || !pathname.startsWith('/') || pathname.startsWith('//')) return false;
482
+ if (pathname.indexOf(proxyBase) === 0) return false;
483
+ return true;
484
+ };
485
+
486
+ const proxiedUrl = (value) => {
487
+ if (typeof value !== 'string') return value;
488
+ if (value.startsWith('/')) {
489
+ if (!shouldProxyPath(value)) return value;
490
+ return proxyBase + value;
491
+ }
492
+
493
+ try {
494
+ const parsed = new URL(value, window.location.href);
495
+ if (parsed.origin === window.location.origin && shouldProxyPath(parsed.pathname)) {
496
+ return proxyBase + parsed.pathname + parsed.search + parsed.hash;
497
+ }
498
+ } catch {}
499
+
500
+ return value;
501
+ };
502
+
503
+ const proxiedWebSocketUrl = (value) => {
504
+ if (typeof value !== 'string') return value;
505
+ try {
506
+ const parsed = new URL(value, window.location.href);
507
+ const current = new URL(window.location.href);
508
+ const sameHost = parsed.host === current.host;
509
+ const isWebSocketProtocol = parsed.protocol === 'ws:' || parsed.protocol === 'wss:';
510
+ if (sameHost && isWebSocketProtocol && shouldProxyPath(parsed.pathname)) {
511
+ parsed.pathname = proxyBase + parsed.pathname;
512
+ return parsed.toString();
513
+ }
514
+ } catch {}
515
+ return value;
516
+ };
517
+
518
+ if (typeof window.fetch === 'function') {
519
+ const nativeFetch = window.fetch.bind(window);
520
+ window.fetch = function(input, init) {
521
+ if (typeof input === 'string') {
522
+ return nativeFetch(proxiedUrl(input), init);
523
+ }
524
+ if (input instanceof Request) {
525
+ try {
526
+ const parsed = new URL(input.url);
527
+ if (parsed.origin === window.location.origin && shouldProxyPath(parsed.pathname)) {
528
+ const nextUrl = proxyBase + parsed.pathname + parsed.search + parsed.hash;
529
+ return nativeFetch(new Request(nextUrl, input), init);
530
+ }
531
+ } catch {}
532
+ }
533
+ return nativeFetch(input, init);
534
+ };
535
+ }
536
+
537
+ if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) {
538
+ const nativeOpen = window.XMLHttpRequest.prototype.open;
539
+ window.XMLHttpRequest.prototype.open = function(method, url) {
540
+ const args = Array.prototype.slice.call(arguments);
541
+ if (typeof url === 'string') {
542
+ args[1] = proxiedUrl(url);
543
+ }
544
+ return nativeOpen.apply(this, args);
545
+ };
546
+ }
547
+
548
+ if (typeof window.EventSource === 'function') {
549
+ const NativeEventSource = window.EventSource;
550
+ function VinciPreviewEventSource(url, eventSourceInitDict) {
551
+ return new NativeEventSource(proxiedUrl(String(url)), eventSourceInitDict);
552
+ }
553
+ VinciPreviewEventSource.prototype = NativeEventSource.prototype;
554
+ Object.setPrototypeOf(VinciPreviewEventSource, NativeEventSource);
555
+ Object.defineProperty(VinciPreviewEventSource, 'name', { value: 'EventSource' });
556
+ window.EventSource = VinciPreviewEventSource;
557
+ }
558
+
559
+ if (typeof window.WebSocket === 'function') {
560
+ const NativeWebSocket = window.WebSocket;
561
+ function VinciPreviewAppWebSocket(url, protocols) {
562
+ const nextUrl = proxiedWebSocketUrl(String(url));
563
+ return arguments.length === 1
564
+ ? new NativeWebSocket(nextUrl)
565
+ : new NativeWebSocket(nextUrl, protocols);
566
+ }
567
+ VinciPreviewAppWebSocket.prototype = NativeWebSocket.prototype;
568
+ Object.setPrototypeOf(VinciPreviewAppWebSocket, NativeWebSocket);
569
+ Object.defineProperty(VinciPreviewAppWebSocket, 'name', { value: 'WebSocket' });
570
+ window.WebSocket = VinciPreviewAppWebSocket;
571
+ }
572
+ };
573
+
574
+ const selectorPart = (element) => {
575
+ const tag = element.tagName.toLowerCase();
576
+ if (element.id && /^[A-Za-z][\w:.-]*$/.test(element.id)) return tag + '#' + CSS.escape(element.id);
577
+ const testId = element.getAttribute('data-testid') || element.getAttribute('data-test') || element.getAttribute('data-cy');
578
+ if (testId) return tag + '[data-testid="' + CSS.escape(testId) + '"]';
579
+ const classes = Array.from(element.classList || []).slice(0, 3).map((entry) => '.' + CSS.escape(entry)).join('');
580
+ return tag + classes;
581
+ };
582
+
583
+ const buildSelector = (element) => {
584
+ const parts = [];
585
+ let current = element;
586
+ while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.documentElement) {
587
+ let part = selectorPart(current);
588
+ const parent = current.parentElement;
589
+ if (parent) {
590
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
591
+ if (siblings.length > 1 && !part.includes('#') && !part.includes('[data-testid=')) {
592
+ part += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')';
593
+ }
594
+ }
595
+ parts.unshift(part);
596
+ if (part.includes('#')) break;
597
+ current = parent;
598
+ }
599
+ return parts.join(' > ');
600
+ };
601
+
602
+ const metadataForElement = (element) => {
603
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;
604
+ const rect = element.getBoundingClientRect();
605
+ const style = window.getComputedStyle(element);
606
+ const attributes = {};
607
+ for (const name of ['id', 'class', 'role', 'aria-label', 'href', 'src', 'data-testid', 'data-test', 'data-cy']) {
608
+ const value = typeof element.getAttribute === 'function' ? element.getAttribute(name) : null;
609
+ if (value) attributes[name] = clip(value, 300);
610
+ }
611
+ const ancestry = [];
612
+ let current = element;
613
+ while (current && current.nodeType === Node.ELEMENT_NODE && ancestry.length < 6) {
614
+ ancestry.unshift({
615
+ tag: current.tagName.toLowerCase(),
616
+ id: current.id || undefined,
617
+ className: clip(current.className || '', 200) || undefined,
618
+ selectorPart: selectorPart(current),
619
+ });
620
+ current = current.parentElement;
621
+ }
622
+ return {
623
+ frame: 'top',
624
+ tag: element.tagName.toLowerCase(),
625
+ text: clip(element.innerText || element.textContent || ''),
626
+ selector: buildSelector(element),
627
+ path: ancestry.map((entry) => entry.tag).join(' > '),
628
+ bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
629
+ center: { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 },
630
+ attributes,
631
+ computedStyle: {
632
+ display: style.display,
633
+ position: style.position,
634
+ color: style.color,
635
+ backgroundColor: style.backgroundColor,
636
+ fontFamily: style.fontFamily,
637
+ fontSize: style.fontSize,
638
+ fontWeight: style.fontWeight,
639
+ lineHeight: style.lineHeight,
640
+ zIndex: style.zIndex,
641
+ },
642
+ ancestry,
643
+ };
644
+ };
645
+
646
+ const hoverKeyForTarget = (target) => {
647
+ if (!target) return '';
648
+ const bounds = target.bounds || {};
649
+ return [target.selector, Math.round(bounds.x), Math.round(bounds.y), Math.round(bounds.width), Math.round(bounds.height)].join('|');
650
+ };
651
+
652
+ const sendHover = (event) => {
653
+ if (!inspectMode) return;
654
+ pendingHover = event;
655
+ if (window.__vinciPreviewHoverFrame) return;
656
+ window.__vinciPreviewHoverFrame = window.requestAnimationFrame(() => {
657
+ window.__vinciPreviewHoverFrame = 0;
658
+ const currentEvent = pendingHover;
659
+ pendingHover = null;
660
+ if (!currentEvent || !inspectMode) return;
661
+ const element = document.elementFromPoint(currentEvent.clientX, currentEvent.clientY);
662
+ const target = metadataForElement(element);
663
+ const key = hoverKeyForTarget(target);
664
+ if (key === lastHoverKey) return;
665
+ lastHoverKey = key;
666
+ post({ type: 'hover', target, pointer: { x: currentEvent.clientX, y: currentEvent.clientY }, ts: Date.now() });
667
+ });
668
+ };
669
+
670
+ const setInspectMode = (enabled) => {
671
+ inspectMode = Boolean(enabled);
672
+ lastHoverKey = '';
673
+ document.documentElement.style.cursor = inspectMode ? 'crosshair' : '';
674
+ if (!inspectMode) {
675
+ post({ type: 'hover', target: null, pointer: { x: 0, y: 0 }, ts: Date.now() });
676
+ }
677
+ };
678
+
679
+ for (const level of ['log', 'info', 'warn', 'error', 'debug']) {
680
+ const original = console[level];
681
+ console[level] = function() {
682
+ const args = Array.prototype.slice.call(arguments);
683
+ if (level === 'debug' && typeof args[0] === 'string' && args[0].indexOf('[vite]') === 0) {
684
+ return original.apply(console, args);
685
+ }
686
+ if (isInternalDevToolConsoleNoise(level, args)) {
687
+ return original.apply(console, args);
688
+ }
689
+ post({ type: 'console', level, args: args.map(stringifyArg), ts: Date.now() });
690
+ return original.apply(console, args);
691
+ };
692
+ }
693
+
694
+ installViteHmrProxyPatch();
695
+ installAppRequestProxyPatch();
696
+
697
+ window.addEventListener('error', (event) => {
698
+ const target = event.target;
699
+ if (target && target !== window && target.nodeType === Node.ELEMENT_NODE) {
700
+ const url = readElementUrl(target);
701
+ if (isInternalDevToolResource(target, url)) {
702
+ return;
703
+ }
704
+ post({
705
+ type: 'resource-error',
706
+ tag: target.tagName.toLowerCase(),
707
+ url: clip(url, 1000),
708
+ outerHTML: clip(target.outerHTML || '', 1000),
709
+ ts: Date.now(),
710
+ });
711
+ return;
712
+ }
713
+ if (isInternalDevToolRuntimeError(event.filename)) {
714
+ return;
715
+ }
716
+ post({
717
+ type: 'runtime-error',
718
+ message: clip(event.message || 'Unknown error', 1000),
719
+ stack: clip(event.error && event.error.stack ? event.error.stack : '', 2000) || undefined,
720
+ filename: event.filename,
721
+ line: event.lineno,
722
+ column: event.colno,
723
+ ts: Date.now(),
724
+ });
725
+ }, true);
726
+
727
+ window.addEventListener('unhandledrejection', (event) => {
728
+ post({
729
+ type: 'runtime-error',
730
+ message: clip(event.reason && event.reason.message ? event.reason.message : event.reason || 'Unhandled promise rejection', 1000),
731
+ stack: clip(event.reason && event.reason.stack ? event.reason.stack : '', 2000) || undefined,
732
+ ts: Date.now(),
733
+ });
734
+ });
735
+
736
+ window.addEventListener('message', (event) => {
737
+ if (event.source !== window.parent) return;
738
+ const data = event.data;
739
+ if (!data || data.source !== 'vinci-preview-parent' || data.version !== VERSION) return;
740
+ if (data.type === 'set-inspect-mode') {
741
+ setInspectMode(data.enabled === true);
742
+ }
743
+ if (data.type === 'set-color-scheme') {
744
+ applyPreviewColorScheme(data.scheme);
745
+ }
746
+ });
747
+
748
+ window.addEventListener('mousemove', sendHover, true);
749
+ window.addEventListener('mouseleave', () => {
750
+ if (!inspectMode) return;
751
+ lastHoverKey = '';
752
+ post({ type: 'hover', target: null, pointer: { x: 0, y: 0 }, ts: Date.now() });
753
+ }, true);
754
+ window.addEventListener('click', (event) => {
755
+ const anchor = event.target && typeof event.target.closest === 'function' ? event.target.closest('a[href]') : null;
756
+ if (anchor && !inspectMode) {
757
+ const navigation = classifyNavigation(anchor.href);
758
+ if (navigation.action === 'proxy' || navigation.action === 'external') {
759
+ event.preventDefault();
760
+ event.stopPropagation();
761
+ post({ type: 'navigate-preview', url: navigation.url, navigation: navigation.action, ts: Date.now() });
762
+ return;
763
+ }
764
+ }
765
+
766
+ if (!inspectMode) return;
767
+ event.preventDefault();
768
+ event.stopPropagation();
769
+ const element = document.elementFromPoint(event.clientX, event.clientY);
770
+ const target = metadataForElement(element);
771
+ if (target) {
772
+ post({ type: 'select', target, pointer: { x: event.clientX, y: event.clientY }, ts: Date.now() });
773
+ }
774
+ }, true);
775
+
776
+ window.addEventListener('DOMContentLoaded', () => {
777
+ post({ type: 'ready', url: window.location.href, title: document.title || '' });
778
+ });
779
+ post({ type: 'ready', url: window.location.href, title: document.title || '' });
780
+ })();`;
781
+
782
+ const parseCookieHeader = (cookieHeader) => {
783
+ const result = new Map();
784
+ if (typeof cookieHeader !== 'string' || cookieHeader.length === 0) {
785
+ return result;
786
+ }
787
+
788
+ const parts = cookieHeader.split(';');
789
+ for (const part of parts) {
790
+ const idx = part.indexOf('=');
791
+ if (idx <= 0) {
792
+ continue;
793
+ }
794
+ const key = part.slice(0, idx).trim();
795
+ const value = part.slice(idx + 1).trim();
796
+ if (!key) {
797
+ continue;
798
+ }
799
+ result.set(key, value);
800
+ }
801
+ return result;
802
+ };
803
+
804
+ const buildCookie = ({
805
+ name,
806
+ value,
807
+ path,
808
+ maxAgeSeconds,
809
+ secure,
810
+ }) => {
811
+ const chunks = [`${name}=${value}`];
812
+ if (path) chunks.push(`Path=${path}`);
813
+ if (typeof maxAgeSeconds === 'number' && Number.isFinite(maxAgeSeconds)) {
814
+ chunks.push(`Max-Age=${Math.max(0, Math.trunc(maxAgeSeconds))}`);
815
+ }
816
+ chunks.push('HttpOnly');
817
+ chunks.push('SameSite=Lax');
818
+ if (secure) chunks.push('Secure');
819
+ return chunks.join('; ');
820
+ };
821
+
822
+ const normalizeLoopbackUrl = (rawUrl) => {
823
+ let url;
824
+ try {
825
+ url = new URL(rawUrl);
826
+ } catch {
827
+ return { ok: false, error: 'Invalid URL' };
828
+ }
829
+
830
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
831
+ return { ok: false, error: 'Only http(s) URLs are supported' };
832
+ }
833
+
834
+ const hostname = url.hostname;
835
+ if (!LOOPBACK_HOSTS.has(hostname)) {
836
+ return { ok: false, error: 'Only loopback hosts are supported' };
837
+ }
838
+
839
+ const port = url.port ? Number.parseInt(url.port, 10) : (url.protocol === 'https:' ? 443 : 80);
840
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) {
841
+ return { ok: false, error: 'Invalid port' };
842
+ }
843
+
844
+ // Normalize common loopback hostnames to IPv4 to avoid environments where
845
+ // `localhost` resolves to ::1 but the dev server only binds IPv4.
846
+ if (hostname === '0.0.0.0' || hostname === 'localhost' || hostname === '::1' || hostname === '[::1]') {
847
+ url.hostname = '127.0.0.1';
848
+ }
849
+
850
+ // Only keep origin here; the proxy path is preserved on the Vinci side.
851
+ return { ok: true, origin: url.origin };
852
+ };
853
+
854
+ export const rewritePreviewBody = ({ bodyText, proxyBasePath, targetOrigin, kind }) => {
855
+ if (typeof bodyText !== 'string' || bodyText.length === 0) {
856
+ return bodyText;
857
+ }
858
+
859
+ const prefix = proxyBasePath.endsWith('/') ? proxyBasePath.slice(0, -1) : proxyBasePath;
860
+ const target = targetOrigin ? new URL(targetOrigin) : null;
861
+ const isSameLoopbackTarget = (url) => {
862
+ if (!target) return false;
863
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
864
+ const host = url.hostname;
865
+ if (host !== 'localhost' && host !== '127.0.0.1' && host !== '0.0.0.0' && host !== '::1' && host !== '[::1]') {
866
+ return false;
867
+ }
868
+ return url.port === target.port;
869
+ };
870
+ const rewriteResourceUrl = (value) => {
871
+ if (typeof value !== 'string' || value.length === 0) return value;
872
+ if (value.startsWith('/') && !value.startsWith('//')) {
873
+ if (value.startsWith('/api/preview/proxy/')) return value;
874
+ return `${prefix}${value}`;
875
+ }
876
+ try {
877
+ const parsed = new URL(value);
878
+ if (isSameLoopbackTarget(parsed)) {
879
+ return `${prefix}${parsed.pathname}${parsed.search}${parsed.hash}`;
880
+ }
881
+ } catch {
882
+ return value;
883
+ }
884
+ return value;
885
+ };
886
+ const rewriteHtml = (text) => text
887
+ .replace(/\b(src|href|action)=(['"])([^'"]*)\2/gi, (_match, attr, quote, value) => {
888
+ return `${attr}=${quote}${rewriteResourceUrl(value)}${quote}`;
889
+ })
890
+ .replace(/\bsrcset=(['"])([^'"]*)\1/gi, (_match, quote, value) => {
891
+ const rewritten = String(value).split(',').map((part) => {
892
+ const trimmed = part.trim();
893
+ if (!trimmed) return trimmed;
894
+ const segments = trimmed.split(/\s+/);
895
+ const url = segments[0] || '';
896
+ segments[0] = rewriteResourceUrl(url);
897
+ return segments.join(' ');
898
+ }).join(', ');
899
+ return `srcset=${quote}${rewritten}${quote}`;
900
+ });
901
+ const rewriteCss = (text) => text
902
+ .replace(/url\((['"]?)([^)'"]*)\1\)/gi, (_match, quote, value) => {
903
+ const q = quote || '';
904
+ return `url(${q}${rewriteResourceUrl(value)}${q})`;
905
+ })
906
+ .replace(/@import\s+(['"])\/(?!\/)([^'"]*)\1/gi, (_match, quote, path) => {
907
+ return `@import ${quote}${rewriteResourceUrl(`/${path}`)}${quote}`;
908
+ });
909
+ const rewriteJavaScript = (text) => text
910
+ .replace(/\bfrom\s+(['"])\/(?!\/)([^'"]*)\1/gi, (_match, quote, path) => {
911
+ return `from ${quote}${rewriteResourceUrl(`/${path}`)}${quote}`;
912
+ })
913
+ .replace(/\bimport\s+(['"])\/(?!\/)([^'"]*)\1/gi, (_match, quote, path) => {
914
+ return `import ${quote}${rewriteResourceUrl(`/${path}`)}${quote}`;
915
+ })
916
+ .replace(/\bimport\(\s*(['"])\/(?!\/)([^'"]*)\1\s*\)/gi, (_match, quote, path) => {
917
+ return `import(${quote}${rewriteResourceUrl(`/${path}`)}${quote})`;
918
+ });
919
+
920
+ if (kind === 'html') return rewriteHtml(bodyText);
921
+ if (kind === 'css') return rewriteCss(bodyText);
922
+ if (kind === 'javascript') return rewriteJavaScript(bodyText);
923
+ return bodyText;
924
+ };
925
+
926
+ export const createPreviewProxyRuntime = ({
927
+ crypto,
928
+ URL,
929
+ createProxyMiddleware,
930
+ responseInterceptor,
931
+ }) => {
932
+ const targets = new Map();
933
+ let sweepTimer = null;
934
+
935
+ const now = () => Date.now();
936
+
937
+ const sweepExpired = () => {
938
+ const t = now();
939
+ for (const [id, entry] of targets.entries()) {
940
+ if (entry.expiresAt <= t) {
941
+ targets.delete(id);
942
+ }
943
+ }
944
+ };
945
+
946
+ const ensureSweeper = () => {
947
+ if (sweepTimer) {
948
+ return;
949
+ }
950
+ sweepTimer = setInterval(sweepExpired, 30_000);
951
+ // Don't keep the process alive.
952
+ sweepTimer.unref?.();
953
+ };
954
+
955
+ const createTarget = (origin, ttlMs) => {
956
+ const id = crypto.randomBytes(16).toString('hex');
957
+ const token = crypto.randomBytes(16).toString('hex');
958
+ const createdAt = now();
959
+ const expiresAt = createdAt + (Number.isFinite(ttlMs) ? Math.max(15_000, Math.trunc(ttlMs)) : DEFAULT_TARGET_TTL_MS);
960
+ targets.set(id, {
961
+ id,
962
+ origin,
963
+ token,
964
+ createdAt,
965
+ expiresAt,
966
+ });
967
+ return { id, token, expiresAt };
968
+ };
969
+
970
+ const resolveTargetFromRequest = (req) => {
971
+ const rawUrl = req?.originalUrl || req?.url || '';
972
+ const parsed = new URL(rawUrl, 'http://localhost');
973
+ const pathname = parsed.pathname || '';
974
+
975
+ const match = pathname.match(/^\/api\/preview\/proxy\/([a-f0-9]{16,64})(?:\/|$)/i);
976
+ const id = match?.[1] || '';
977
+ if (!id) {
978
+ return { ok: false, status: 404, error: 'Preview target not found' };
979
+ }
980
+
981
+ const entry = targets.get(id);
982
+ if (!entry || entry.expiresAt <= now()) {
983
+ targets.delete(id);
984
+ return { ok: false, status: 404, error: 'Preview target expired' };
985
+ }
986
+
987
+ const cookies = parseCookieHeader(req.headers?.cookie);
988
+ const token = cookies.get(TOKEN_COOKIE_NAME) || '';
989
+ if (!token || token !== entry.token) {
990
+ return { ok: false, status: 403, error: 'Preview token missing' };
991
+ }
992
+
993
+ return { ok: true, id, entry, parsed };
994
+ };
995
+
996
+ const stripProxyPrefix = (pathname, id) => {
997
+ const prefix = `/api/preview/proxy/${id}`;
998
+ if (!pathname.startsWith(prefix)) {
999
+ return pathname;
1000
+ }
1001
+ const rest = pathname.slice(prefix.length);
1002
+ return rest.length === 0 ? '/' : rest;
1003
+ };
1004
+
1005
+ const removeRawQueryParam = (search, paramName) => {
1006
+ if (typeof search !== 'string' || search.length <= 1) {
1007
+ return '';
1008
+ }
1009
+ const query = search.startsWith('?') ? search.slice(1) : search;
1010
+ const parts = query.split('&').filter((part) => {
1011
+ const name = part.split('=', 1)[0] || '';
1012
+ return decodeURIComponent(name.replace(/\+/g, ' ')) !== paramName;
1013
+ });
1014
+ return parts.length > 0 ? `?${parts.join('&')}` : '';
1015
+ };
1016
+
1017
+ // Strip the `frame-ancestors` directive from a CSP header value while
1018
+ // preserving every other directive. Returns null if no directives remain.
1019
+ const removeFrameAncestorsDirective = (cspValue) => {
1020
+ if (typeof cspValue !== 'string' || cspValue.length === 0) {
1021
+ return cspValue;
1022
+ }
1023
+ const directives = cspValue
1024
+ .split(';')
1025
+ .map((part) => part.trim())
1026
+ .filter((part) => part.length > 0);
1027
+
1028
+ const filtered = directives.filter((directive) => {
1029
+ const name = directive.split(/\s+/, 1)[0]?.toLowerCase() ?? '';
1030
+ return name !== 'frame-ancestors';
1031
+ });
1032
+
1033
+ if (filtered.length === 0) {
1034
+ return null;
1035
+ }
1036
+ return filtered.join('; ');
1037
+ };
1038
+
1039
+ // Drop response headers that prevent the dev server from being framed.
1040
+ // The proxy itself is same-origin, so embedding is otherwise safe.
1041
+ const stripFrameBustingHeaders = (headers) => {
1042
+ if (!headers || typeof headers !== 'object') {
1043
+ return;
1044
+ }
1045
+
1046
+ const headerKeys = Object.keys(headers);
1047
+ for (const key of headerKeys) {
1048
+ const lowerKey = key.toLowerCase();
1049
+ if (lowerKey === 'x-frame-options') {
1050
+ delete headers[key];
1051
+ continue;
1052
+ }
1053
+ if (lowerKey === 'content-security-policy' || lowerKey === 'content-security-policy-report-only') {
1054
+ const original = headers[key];
1055
+ const values = Array.isArray(original) ? original : [original];
1056
+ const rewritten = values
1057
+ .map((value) => removeFrameAncestorsDirective(value))
1058
+ .filter((value) => typeof value === 'string' && value.length > 0);
1059
+ if (rewritten.length === 0) {
1060
+ delete headers[key];
1061
+ } else {
1062
+ headers[key] = Array.isArray(original) ? rewritten : rewritten[0];
1063
+ }
1064
+ }
1065
+ }
1066
+ };
1067
+
1068
+ const attach = (app, {
1069
+ server,
1070
+ express,
1071
+ uiAuthController,
1072
+ isRequestOriginAllowed,
1073
+ rejectWebSocketUpgrade,
1074
+ }) => {
1075
+ ensureSweeper();
1076
+
1077
+ const injectPreviewBridge = (bodyText) => {
1078
+ if (typeof bodyText !== 'string' || bodyText.includes(PREVIEW_BRIDGE_SCRIPT_ID)) {
1079
+ return bodyText;
1080
+ }
1081
+
1082
+ const script = `<script id="${PREVIEW_BRIDGE_SCRIPT_ID}">${PREVIEW_BRIDGE_SCRIPT}</script>`;
1083
+ if (/<head(?:\s[^>]*)?>/i.test(bodyText)) {
1084
+ return bodyText.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${script}`);
1085
+ }
1086
+ if (bodyText.includes('</body>')) {
1087
+ return bodyText.replace('</body>', `${script}</body>`);
1088
+ }
1089
+ return `${bodyText}${script}`;
1090
+ };
1091
+
1092
+ const rewriteViteClientHmr = (bodyText, proxyBasePath) => {
1093
+ if (typeof bodyText !== 'string' || !bodyText.includes('vite-hmr')) {
1094
+ return bodyText;
1095
+ }
1096
+
1097
+ const base = proxyBasePath.endsWith('/') ? proxyBasePath : `${proxyBasePath}/`;
1098
+ const escapedBase = JSON.stringify(base).slice(1, -1);
1099
+ return bodyText
1100
+ .replace(/const base\$1 = [^;]+;/, () => `const base$1 = ${JSON.stringify(base)};`)
1101
+ .replace(/const base = [^;]+;/, () => `const base = ${JSON.stringify(base)};`)
1102
+ .replace(/const hmrPort = [^;]+;/, () => 'const hmrPort = importMetaUrl.port;')
1103
+ .replace(/const socketHost = [^;]+;/, () => `const socketHost = \`\${importMetaUrl.hostname}\${importMetaUrl.port ? ':' + importMetaUrl.port : ''}${escapedBase}\`;`)
1104
+ .replace(/const directSocketHost = [^;]+;/, () => 'const directSocketHost = socketHost;')
1105
+ .replace(
1106
+ /const socketHost = `\$\{[^;]+?;\nconst directSocketHost = [^;]+;/s,
1107
+ () => `const socketHost = \`\${importMetaUrl.hostname}\${importMetaUrl.port ? ':' + importMetaUrl.port : ''}${escapedBase}\`;\nconst directSocketHost = socketHost;`,
1108
+ );
1109
+ };
1110
+
1111
+ app.post('/api/preview/targets', express.json(), async (req, res) => {
1112
+ try {
1113
+ if (uiAuthController?.enabled) {
1114
+ const sessionToken = await uiAuthController?.ensureSessionToken?.(req, res);
1115
+ if (!sessionToken) {
1116
+ return res.status(401).json({ error: 'UI authentication required' });
1117
+ }
1118
+
1119
+ const originAllowed = await isRequestOriginAllowed(req);
1120
+ if (!originAllowed) {
1121
+ return res.status(403).json({ error: 'Invalid origin' });
1122
+ }
1123
+ }
1124
+
1125
+ const rawUrl = typeof req.body?.url === 'string' ? req.body.url.trim() : '';
1126
+ if (!rawUrl) {
1127
+ return res.status(400).json({ error: 'url is required' });
1128
+ }
1129
+
1130
+ const ttlMs = typeof req.body?.ttlMs === 'number' ? req.body.ttlMs : DEFAULT_TARGET_TTL_MS;
1131
+ const normalized = normalizeLoopbackUrl(rawUrl);
1132
+ if (!normalized.ok) {
1133
+ return res.status(400).json({ error: normalized.error });
1134
+ }
1135
+
1136
+ const target = createTarget(normalized.origin, ttlMs);
1137
+ const cookiePath = `/api/preview/proxy/${target.id}`;
1138
+ const secure = Boolean(req.secure);
1139
+ res.setHeader('Set-Cookie', buildCookie({
1140
+ name: TOKEN_COOKIE_NAME,
1141
+ value: target.token,
1142
+ path: cookiePath,
1143
+ maxAgeSeconds: Math.round((target.expiresAt - now()) / 1000),
1144
+ secure,
1145
+ }));
1146
+
1147
+ return res.json({
1148
+ id: target.id,
1149
+ proxyBasePath: cookiePath,
1150
+ expiresAt: target.expiresAt,
1151
+ });
1152
+ } catch (error) {
1153
+ console.error('[preview-proxy] Failed to create target:', error);
1154
+ return res.status(500).json({ error: 'Failed to create preview target' });
1155
+ }
1156
+ });
1157
+
1158
+ const proxy = createProxyMiddleware({
1159
+ target: 'http://127.0.0.1',
1160
+ changeOrigin: true,
1161
+ ws: true,
1162
+ selfHandleResponse: true,
1163
+ // Restrict the proxy (especially its auto-attached `upgrade` listener,
1164
+ // which is registered globally on the underlying HTTP server when
1165
+ // `ws: true`) to preview paths. Without this, every WebSocket upgrade
1166
+ // on the server (e.g. `/api/terminal/ws`) gets proxied to
1167
+ // `http://127.0.0.1` and tears the socket down with ECONNREFUSED.
1168
+ //
1169
+ // We use a function so the same filter handles both cases:
1170
+ // - HTTP requests through Express, where `req.url` has been stripped
1171
+ // of the `/api/preview/proxy` mount-point, so we check `originalUrl`.
1172
+ // - Raw upgrade events from the HTTP server, where `req.url` still
1173
+ // contains the full path.
1174
+ pathFilter: (pathname, req) => {
1175
+ const target = req?.originalUrl || pathname || req?.url || '';
1176
+ return target.startsWith('/api/preview/proxy/');
1177
+ },
1178
+ router: (req) => {
1179
+ const resolved = resolveTargetFromRequest(req);
1180
+ if (!resolved.ok) {
1181
+ return 'http://127.0.0.1';
1182
+ }
1183
+ return resolved.entry.origin;
1184
+ },
1185
+ pathRewrite: (pathValue, req) => {
1186
+ const resolved = resolveTargetFromRequest(req);
1187
+ if (!resolved.ok) {
1188
+ return pathValue;
1189
+ }
1190
+
1191
+ const parsed = new URL(req.originalUrl || req.url || '', 'http://localhost');
1192
+ // Never forward our auth cookie token to the dev server.
1193
+ const strippedPath = stripProxyPrefix(parsed.pathname, resolved.id);
1194
+ return `${strippedPath}${removeRawQueryParam(parsed.search, 'ocPreview')}`;
1195
+ },
1196
+ on: {
1197
+ proxyReq: (proxyReq) => {
1198
+ // Keep local dev servers from receiving Vinci credentials.
1199
+ proxyReq.removeHeader('cookie');
1200
+ proxyReq.removeHeader('authorization');
1201
+ proxyReq.removeHeader('x-vinci-ui-session');
1202
+ proxyReq.setHeader('accept-encoding', 'identity');
1203
+ },
1204
+ proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req) => {
1205
+ // Allow the dev server response to be framed inside Vinci even
1206
+ // if it normally sets X-Frame-Options or a CSP frame-ancestors rule.
1207
+ // The proxy is same-origin so embedding is otherwise safe.
1208
+ stripFrameBustingHeaders(proxyRes.headers);
1209
+
1210
+ const contentType = String(proxyRes.headers?.['content-type'] || '').toLowerCase();
1211
+ const isHtml = contentType.includes('text/html');
1212
+ const isCss = contentType.includes('text/css');
1213
+ const isJavaScript = contentType.includes('javascript') || contentType.includes('ecmascript');
1214
+ if (!isHtml && !isCss && !isJavaScript) {
1215
+ return responseBuffer;
1216
+ }
1217
+
1218
+ proxyRes.headers['cache-control'] = 'no-store, no-cache, must-revalidate, proxy-revalidate';
1219
+ proxyRes.headers.pragma = 'no-cache';
1220
+ proxyRes.headers.expires = '0';
1221
+ delete proxyRes.headers.etag;
1222
+ delete proxyRes.headers['last-modified'];
1223
+
1224
+ const resolved = resolveTargetFromRequest(req);
1225
+ if (!resolved.ok) {
1226
+ return responseBuffer;
1227
+ }
1228
+
1229
+ const proxyBasePath = `/api/preview/proxy/${resolved.id}`;
1230
+ const parsed = new URL(req.originalUrl || req.url || '', 'http://localhost');
1231
+ const upstreamPath = stripProxyPrefix(parsed.pathname, resolved.id);
1232
+ if (isJavaScript && upstreamPath === '/@vite/client') {
1233
+ return rewritePreviewBody({
1234
+ bodyText: rewriteViteClientHmr(responseBuffer.toString('utf8'), proxyBasePath),
1235
+ proxyBasePath,
1236
+ targetOrigin: resolved.entry.origin,
1237
+ kind: 'javascript',
1238
+ });
1239
+ }
1240
+
1241
+ const rewrittenBody = rewritePreviewBody({
1242
+ bodyText: responseBuffer.toString('utf8'),
1243
+ proxyBasePath,
1244
+ targetOrigin: resolved.entry.origin,
1245
+ kind: isHtml ? 'html' : isCss ? 'css' : 'javascript',
1246
+ });
1247
+ return isHtml ? injectPreviewBridge(rewrittenBody) : rewrittenBody;
1248
+ }),
1249
+ error: (err, _req, res) => {
1250
+ const isDev = typeof process !== 'undefined'
1251
+ && process
1252
+ && process.env
1253
+ && process.env.NODE_ENV !== 'production';
1254
+
1255
+ const message = err && typeof err === 'object' && typeof err.message === 'string'
1256
+ ? err.message
1257
+ : 'Unknown proxy error';
1258
+
1259
+ console.error('[preview-proxy] proxy error:', message);
1260
+
1261
+ if (res && !res.headersSent && typeof res.status === 'function') {
1262
+ const payload = { error: 'Preview proxy error' };
1263
+
1264
+ if (isDev) {
1265
+ try {
1266
+ const resolved = resolveTargetFromRequest(_req);
1267
+ payload.details = {
1268
+ message,
1269
+ code: err && typeof err === 'object' ? err.code : undefined,
1270
+ targetOrigin: resolved?.ok ? resolved.entry.origin : undefined,
1271
+ };
1272
+ } catch {
1273
+ payload.details = { message };
1274
+ }
1275
+ }
1276
+
1277
+ res.status(502).json(payload);
1278
+ }
1279
+ },
1280
+ },
1281
+ });
1282
+
1283
+ app.use('/api/preview/proxy', (req, res, next) => {
1284
+ const resolved = resolveTargetFromRequest(req);
1285
+ if (!resolved.ok) {
1286
+ return res.status(resolved.status).json({ error: resolved.error });
1287
+ }
1288
+ next();
1289
+ }, proxy);
1290
+
1291
+ server.on('upgrade', (req, socket, head) => {
1292
+ const resolved = resolveTargetFromRequest(req);
1293
+ if (!resolved.ok) {
1294
+ return;
1295
+ }
1296
+
1297
+ const handleUpgrade = async () => {
1298
+ try {
1299
+ if (uiAuthController?.enabled) {
1300
+ const sessionToken = await uiAuthController?.ensureSessionToken?.(req, null);
1301
+ if (!sessionToken) {
1302
+ rejectWebSocketUpgrade(socket, 401, 'UI authentication required');
1303
+ return;
1304
+ }
1305
+
1306
+ const originAllowed = await isRequestOriginAllowed(req);
1307
+ if (!originAllowed) {
1308
+ rejectWebSocketUpgrade(socket, 403, 'Invalid origin');
1309
+ return;
1310
+ }
1311
+ }
1312
+
1313
+ // Rewrite req.url to what the dev server expects.
1314
+ const rawUrl = req.url || '';
1315
+ req.originalUrl = rawUrl;
1316
+ const parsed = new URL(rawUrl, 'http://localhost');
1317
+ const nextPath = stripProxyPrefix(parsed.pathname, resolved.id);
1318
+ const search = parsed.searchParams.toString();
1319
+ req.url = `${nextPath}${search ? `?${search}` : ''}`;
1320
+ proxy.upgrade(req, socket, head);
1321
+ } catch {
1322
+ rejectWebSocketUpgrade(socket, 500, 'Upgrade failed');
1323
+ }
1324
+ };
1325
+
1326
+ void handleUpgrade();
1327
+ });
1328
+ };
1329
+
1330
+ return {
1331
+ attach,
1332
+ };
1333
+ };