@vantran-se/openchamber-web 1.23.1 → 1.24.4

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 (313) hide show
  1. package/bin/cli.test.js +29 -6
  2. package/bin/lib/cli-http.js +5 -1
  3. package/bin/lib/commands-status.js +5 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-BOpqXpFU.js → CodeMirrorCommentWidgets-B4a0JkUz.js} +1 -1
  5. package/dist/assets/DiffView-B2CD9wC8.js +4 -0
  6. package/dist/assets/FilesView-C4vMFclc.js +13 -0
  7. package/dist/assets/GitView-Byu2DY7s.js +6 -0
  8. package/dist/assets/GuestAttachDialog-B808E0-d.js +1 -0
  9. package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-VW_7czo3.js} +1 -1
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/LinearIssuesView-EzGNCGrd.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-B2YUDFaB.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +12 -0
  72. package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-CQHBSTEi.js} +1 -1
  73. package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DCjGhlYV.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-CEDZNkU2.js +210 -0
  75. package/dist/assets/PlanView-D3x-pTkZ.js +5 -0
  76. package/dist/assets/PluginPane-DEcgDQ9h.js +3 -0
  77. package/dist/assets/SettingsView-CSinuH-1.js +29 -0
  78. package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-B5CHoJFZ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +1 -0
  80. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CflzmK-x.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-NU0FHFhT.js} +3 -3
  83. package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-BZyjhqHv.js} +2 -2
  84. package/dist/assets/appThemeRegistry-DLicw4dH.js +1 -0
  85. package/dist/assets/commitAndPush-BDftrjr6.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DlWlEM3x.js} +1 -1
  87. package/dist/assets/de-Bro6_zyl.js +10 -0
  88. package/dist/assets/es-Bb9XQZvd.js +16 -0
  89. package/dist/assets/fr-Dgfzn-IL.js +14 -0
  90. package/dist/assets/{index-eo1-D_7z.js → index-CtQNmqUL.js} +1 -1
  91. package/dist/assets/index-DqaMWNrC.css +1 -0
  92. package/dist/assets/index-Dqc6USl3.js +40 -0
  93. package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-CO4e5J_S.js} +1 -1
  94. package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-ue6kCou7.js} +1 -1
  95. package/dist/assets/ja-BYsakGFh.js +16 -0
  96. package/dist/assets/ko-BSaQz9aN.js +16 -0
  97. package/dist/assets/main-B62_pjL5.js +2 -0
  98. package/dist/assets/main-BEdcOI2P.js +896 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-CK48LG0z.js +5 -0
  101. package/dist/assets/miniChat-LT8Rqofr.js +2 -0
  102. package/dist/assets/mobile-CqarEynh.js +2 -0
  103. package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-D7QVlovD.js} +3 -3
  104. package/dist/assets/pl-OIviPdoj.js +16 -0
  105. package/dist/assets/pt-BR-Cltg-E6s.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +2 -0
  107. package/dist/assets/renderMobileApp-BC1MHg6g.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +25 -0
  109. package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-B2SWMGEU.js} +1 -1
  110. package/dist/assets/shikiHighlight-BPu3JiQs.js +1 -0
  111. package/dist/assets/textMateThemeFromAppTheme-DWqRPo9A.js +1 -0
  112. package/dist/assets/tr-CQI-KPre.js +7 -0
  113. package/dist/assets/uk-CqW7R4DA.js +16 -0
  114. package/dist/assets/useAppFontEffects-ne84JBVL.js +7333 -0
  115. package/dist/assets/useInlineCommentController-CgS0fnw_.js +2 -0
  116. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +2 -0
  117. package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  118. package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-Bx2EHc1O.js} +2 -2
  119. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  120. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  121. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  122. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  123. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  124. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  125. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  126. package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
  127. package/dist/assets/{web-components-B_Z64So5.js → web-components-DxX8bZ1o.js} +2 -2
  128. package/dist/assets/zh-CN-CVZG_o_J.js +16 -0
  129. package/dist/assets/zh-TW-D5ZxzCZp.js +16 -0
  130. package/dist/index.html +6 -4
  131. package/dist/mini-chat.html +6 -4
  132. package/dist/mobile.html +6 -4
  133. package/package.json +12 -9
  134. package/server/built-in-extensions/registry.json +4 -0
  135. package/server/index.d.ts +2 -0
  136. package/server/index.js +44 -10
  137. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  138. package/server/lib/agent-tool/runtime.js +62 -15
  139. package/server/lib/agent-tool/runtime.test.js +88 -2
  140. package/server/lib/changelog/update-notes.js +2 -1
  141. package/server/lib/changelog/update-notes.test.js +19 -0
  142. package/server/lib/event-stream/DOCUMENTATION.md +23 -0
  143. package/server/lib/event-stream/delta-coalescer.js +0 -0
  144. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  145. package/server/lib/event-stream/global-hub.js +53 -23
  146. package/server/lib/event-stream/global-hub.test.js +160 -0
  147. package/server/lib/event-stream/global-ws-bridge.js +1 -0
  148. package/server/lib/event-stream/index.js +4 -0
  149. package/server/lib/fs/DOCUMENTATION.md +10 -1
  150. package/server/lib/fs/git-process.test.js +77 -0
  151. package/server/lib/fs/routes.js +58 -5
  152. package/server/lib/fs/routes.test.js +116 -0
  153. package/server/lib/fs/search.js +2 -1
  154. package/server/lib/git/DOCUMENTATION.md +20 -5
  155. package/server/lib/git/routes.js +16 -3
  156. package/server/lib/git/serial-refresh.js +93 -0
  157. package/server/lib/git/serial-refresh.test.js +112 -0
  158. package/server/lib/git/service.js +429 -70
  159. package/server/lib/git/service.test.js +418 -0
  160. package/server/lib/github/pr-status.js +5 -5
  161. package/server/lib/github/pr-status.test.js +1 -1
  162. package/server/lib/github/routes.js +4 -4
  163. package/server/lib/guests/DOCUMENTATION.md +68 -0
  164. package/server/lib/guests/auth-store.js +184 -0
  165. package/server/lib/guests/auth-store.test.js +112 -0
  166. package/server/lib/guests/background.test.js +83 -0
  167. package/server/lib/guests/builtins.js +34 -0
  168. package/server/lib/guests/builtins.test.js +174 -0
  169. package/server/lib/guests/catalog.js +477 -0
  170. package/server/lib/guests/catalog.test.js +390 -0
  171. package/server/lib/guests/clone.js +297 -0
  172. package/server/lib/guests/extract-zip.js +136 -0
  173. package/server/lib/guests/extract-zip.test.js +109 -0
  174. package/server/lib/guests/files.js +308 -0
  175. package/server/lib/guests/files.test.js +262 -0
  176. package/server/lib/guests/grant-scope.js +81 -0
  177. package/server/lib/guests/grant-scope.test.js +75 -0
  178. package/server/lib/guests/host-session.js +73 -0
  179. package/server/lib/guests/host-session.test.js +30 -0
  180. package/server/lib/guests/html-styles.js +6 -0
  181. package/server/lib/guests/html-styles.test.js +45 -0
  182. package/server/lib/guests/html-tokens.js +73 -0
  183. package/server/lib/guests/html-tokens.test.js +53 -0
  184. package/server/lib/guests/install.js +406 -0
  185. package/server/lib/guests/install.test.js +338 -0
  186. package/server/lib/guests/oauth.js +501 -0
  187. package/server/lib/guests/oauth.test.js +481 -0
  188. package/server/lib/guests/persist.js +336 -0
  189. package/server/lib/guests/persist.test.js +288 -0
  190. package/server/lib/guests/request.js +112 -0
  191. package/server/lib/guests/request.test.js +206 -0
  192. package/server/lib/guests/routes.js +838 -0
  193. package/server/lib/guests/service.js +538 -0
  194. package/server/lib/guests/service.test.js +277 -0
  195. package/server/lib/guests/sockets.js +129 -0
  196. package/server/lib/guests/sockets.test.js +110 -0
  197. package/server/lib/guests/ssh-install.test.js +188 -0
  198. package/server/lib/guests/storage.js +68 -0
  199. package/server/lib/guests/storage.test.js +59 -0
  200. package/server/lib/guests/updates.js +294 -0
  201. package/server/lib/guests/updates.test.js +334 -0
  202. package/server/lib/guests/upload.js +50 -0
  203. package/server/lib/guests/upload.test.js +131 -0
  204. package/server/lib/linear/DOCUMENTATION.md +1 -1
  205. package/server/lib/message-queue/DOCUMENTATION.md +10 -1
  206. package/server/lib/message-queue/runtime.js +21 -1
  207. package/server/lib/message-queue/runtime.test.js +52 -5
  208. package/server/lib/notifications/routes.js +5 -0
  209. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  210. package/server/lib/openchamber-sessions/routes.js +62 -29
  211. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  212. package/server/lib/opencode/DOCUMENTATION.md +55 -1
  213. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  214. package/server/lib/opencode/core-routes.js +39 -1
  215. package/server/lib/opencode/feature-routes-runtime.js +15 -0
  216. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  217. package/server/lib/opencode/lifecycle.js +54 -67
  218. package/server/lib/opencode/openchamber-routes.js +72 -14
  219. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  220. package/server/lib/opencode/session-runtime.js +68 -0
  221. package/server/lib/opencode/session-runtime.test.js +43 -0
  222. package/server/lib/opencode/settings-helpers.js +8 -0
  223. package/server/lib/opencode/settings-helpers.test.js +13 -1
  224. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  225. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  226. package/server/lib/opencode/settings-registry.json +15 -0
  227. package/server/lib/opencode/shutdown-runtime.js +3 -0
  228. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  229. package/server/lib/opencode/theme-archive.js +53 -0
  230. package/server/lib/opencode/theme-catalog.js +171 -0
  231. package/server/lib/opencode/theme-catalog.test.js +125 -0
  232. package/server/lib/opencode/theme-import.test.js +144 -0
  233. package/server/lib/opencode/theme-runtime.js +100 -28
  234. package/server/lib/opencode/theme-runtime.test.js +15 -0
  235. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  236. package/server/lib/permission-auto-accept/runtime.js +13 -0
  237. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  238. package/server/lib/projects/project-config.js +2 -2
  239. package/server/lib/quota/DOCUMENTATION.md +0 -1
  240. package/server/lib/quota/providers/index.js +0 -7
  241. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  242. package/server/lib/realtime-proxy.js +2 -0
  243. package/server/lib/routing/DOCUMENTATION.md +83 -0
  244. package/server/lib/routing/defaults.js +98 -0
  245. package/server/lib/routing/feature-flag.js +11 -0
  246. package/server/lib/routing/history.js +34 -0
  247. package/server/lib/routing/jev.js +98 -0
  248. package/server/lib/routing/routes.http.test.js +100 -0
  249. package/server/lib/routing/routes.js +81 -0
  250. package/server/lib/routing/runtime.js +217 -0
  251. package/server/lib/routing/runtime.test.js +188 -0
  252. package/server/lib/routing/store.js +249 -0
  253. package/server/lib/routing/store.test.js +96 -0
  254. package/server/lib/scheduled-tasks/runtime.js +2 -2
  255. package/server/lib/scheduled-tasks/runtime.test.js +14 -0
  256. package/server/lib/security/request-security.js +9 -5
  257. package/server/lib/security/request-security.test.js +37 -0
  258. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  259. package/server/lib/small-model/call.js +37 -6
  260. package/server/lib/small-model/call.test.js +182 -0
  261. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  262. package/server/lib/terminal/runtime.js +27 -5
  263. package/server/lib/terminal/runtime.test.js +67 -0
  264. package/server/lib/terminal/shutdown.js +106 -0
  265. package/server/lib/terminal/shutdown.test.js +84 -0
  266. package/server/lib/tts/service.test.js +78 -0
  267. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  268. package/server/lib/ui-auth/session-cookie.js +51 -0
  269. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  270. package/server/lib/ui-auth/ui-auth.js +56 -17
  271. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  272. package/dist/assets/DiffView-CyLmb-wF.js +0 -4
  273. package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
  274. package/dist/assets/FilesView-WtJB0y_l.js +0 -13
  275. package/dist/assets/GitView-DkNrV0BK.js +0 -6
  276. package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
  277. package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
  278. package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
  279. package/dist/assets/PlanView-CAFziKH5.js +0 -5
  280. package/dist/assets/SettingsView-BRittavZ.js +0 -28
  281. package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
  282. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  283. package/dist/assets/de-c7CYLCAW.js +0 -10
  284. package/dist/assets/es-BKAVPQmQ.js +0 -16
  285. package/dist/assets/fr-Bj1glqfV.js +0 -14
  286. package/dist/assets/index-BYFyOu_D.css +0 -1
  287. package/dist/assets/index-BfwRClDe.js +0 -40
  288. package/dist/assets/ja-5jwSIbK0.js +0 -16
  289. package/dist/assets/ko-BaDbg-Eb.js +0 -16
  290. package/dist/assets/main-CGG2_uHq.js +0 -2
  291. package/dist/assets/main-CcrSaL4J.js +0 -930
  292. package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
  293. package/dist/assets/miniChat-DEaAhP4b.js +0 -2
  294. package/dist/assets/mobile-BC3rsvv0.js +0 -2
  295. package/dist/assets/pl-BFKVBDFu.js +0 -16
  296. package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
  297. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  298. package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
  299. package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
  300. package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
  301. package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
  302. package/dist/assets/tr-DYXPoEDG.js +0 -7
  303. package/dist/assets/uk-CwAoloR8.js +0 -16
  304. package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
  305. package/dist/assets/useGitComparison-CpxioduU.js +0 -1
  306. package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
  307. package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
  308. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  309. package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
  310. package/dist/assets/zh-TW-1HNK7shh.js +0 -16
  311. package/server/lib/quota/providers/crof.js +0 -96
  312. package/server/lib/quota/providers/crof.test.js +0 -98
  313. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
 
3
3
  import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
4
+ import { isRoutingFeatureAvailable } from '../routing/feature-flag.js';
4
5
 
5
6
  // Generated from packages/ui/src/lib/settings/registry.ts by
6
7
  // `bun run settings-registry:generate`; `registry.test.ts` fails when stale.
@@ -233,6 +234,11 @@ export const createSettingsHelpers = (dependencies) => {
233
234
  if (typeof candidate.workStatusHiddenSectionsExplicit === 'boolean') {
234
235
  result.workStatusHiddenSectionsExplicit = candidate.workStatusHiddenSectionsExplicit;
235
236
  }
237
+ if (Array.isArray(candidate.workStatusSectionOrder)) {
238
+ result.workStatusSectionOrder = [
239
+ ...new Set(candidate.workStatusSectionOrder.filter((entry) => typeof entry === 'string' && entry.length > 0)),
240
+ ];
241
+ }
236
242
  if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
237
243
  result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
238
244
  }
@@ -1008,6 +1014,8 @@ export const createSettingsHelpers = (dependencies) => {
1008
1014
  // Tells the client whether agent memory exists in this build at all, so
1009
1015
  // its settings row and panel tab can be absent rather than merely off.
1010
1016
  agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
1017
+ // Same idea for Jev routing: absent from the picker and Settings unless the build has it.
1018
+ routingFeatureAvailable: isRoutingFeatureAvailable(),
1011
1019
  ...(pwaAppName ? { pwaAppName } : {}),
1012
1020
  pwaOrientation,
1013
1021
  mobileKeyboardMode,
@@ -69,6 +69,18 @@ const createTestHelpersWithRealSanitizers = () => {
69
69
  };
70
70
 
71
71
  describe('settings helpers', () => {
72
+ it('round-trips section order and preserves it across unrelated writes', () => {
73
+ const helpers = createTestHelpers();
74
+ const changes = helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: ['mcp', 'session', 'mcp', null, ''] });
75
+ expect(changes.workStatusSectionOrder).toEqual(['mcp', 'session']);
76
+ const saved = helpers.mergePersistedSettings({}, changes);
77
+ const reloaded = helpers.formatSettingsResponse(JSON.parse(JSON.stringify(saved)));
78
+ expect(reloaded.workStatusSectionOrder).toEqual(['mcp', 'session']);
79
+ const next = helpers.mergePersistedSettings(reloaded, helpers.sanitizeSettingsUpdate({ workStatusPanelEnabled: false }));
80
+ expect(helpers.formatSettingsResponse(next).workStatusSectionOrder).toEqual(['mcp', 'session']);
81
+ expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: 'bad' }).workStatusSectionOrder).toBeUndefined();
82
+ expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: [] }).workStatusSectionOrder).toEqual([]);
83
+ });
72
84
  it('round-trips telemetry opt-in with the hidden list and preserves it across unrelated writes', () => {
73
85
  const helpers = createTestHelpers();
74
86
  const legacy = helpers.sanitizeSettingsUpdate({ workStatusHiddenSections: [] });
@@ -762,7 +774,7 @@ describe('settings registry gate', () => {
762
774
  managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
763
775
  managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
764
776
  sidebarProjectDisplayMode: 'all', sidebarSessionGroupingMode: 'flat', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
765
- workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true,
777
+ workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true, workStatusSectionOrder: ['mcp', 'session'],
766
778
  showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
767
779
  chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
768
780
  stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
@@ -154,6 +154,7 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
154
154
  : null;
155
155
  const iconBackground = normalizeIconBackground(candidate.iconBackground);
156
156
  const color = typeof candidate.color === 'string' ? candidate.color.trim() : '';
157
+ const defaultAgent = typeof candidate.defaultAgent === 'string' ? candidate.defaultAgent.trim() : '';
157
158
  const defaultModel = typeof candidate.defaultModel === 'string' ? candidate.defaultModel.trim() : '';
158
159
  const defaultVariant = typeof candidate.defaultVariant === 'string' ? candidate.defaultVariant.trim() : '';
159
160
  const addedAt = Number.isFinite(candidate.addedAt) ? Number(candidate.addedAt) : null;
@@ -175,6 +176,7 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
175
176
  ...(icon ? { icon } : {}),
176
177
  ...(iconBackground ? { iconBackground } : {}),
177
178
  ...(color ? { color } : {}),
179
+ ...(defaultAgent ? { defaultAgent } : {}),
178
180
  ...(defaultModel && defaultModel.includes('/') ? { defaultModel } : {}),
179
181
  // A variant is meaningless without the model it belongs to.
180
182
  ...(defaultModel && defaultModel.includes('/') && defaultVariant ? { defaultVariant } : {}),
@@ -122,6 +122,19 @@ describe('settings normalization runtime - symlink resolution', () => {
122
122
  expect(result[1].defaultVariant).toBe(undefined);
123
123
  });
124
124
 
125
+ it('keeps a project default agent', () => {
126
+ const runtime = createTestRuntime({
127
+ realpathSync: (p) => p,
128
+ path: { resolve: (p) => p, sep: '/', dirname: (p) => p.split('/').slice(0, -1).join('/') || '/' },
129
+ });
130
+
131
+ const result = runtime.sanitizeProjects([
132
+ { id: 'proj1', path: '/a', defaultAgent: 'plan' },
133
+ ]);
134
+
135
+ expect(result[0].defaultAgent).toBe('plan');
136
+ });
137
+
125
138
  it('deduplicates projects that resolve to the same realpath', () => {
126
139
  const runtime = createTestRuntime({
127
140
  realpathSync: (p) => p.startsWith('/symlink') ? '/real/project' : p,
@@ -127,6 +127,10 @@
127
127
  "scope": "instance",
128
128
  "computed": true
129
129
  },
130
+ "routingFeatureAvailable": {
131
+ "scope": "instance",
132
+ "computed": true
133
+ },
130
134
  "openCodeUpdateToastDismissedVersion": {
131
135
  "scope": "instance"
132
136
  },
@@ -217,6 +221,9 @@
217
221
  "sidebarShowRecentSection": {
218
222
  "scope": "profile"
219
223
  },
224
+ "workStatusSectionOrder": {
225
+ "scope": "profile"
226
+ },
220
227
  "workStatusPanelEnabled": {
221
228
  "scope": "profile"
222
229
  },
@@ -605,6 +612,10 @@
605
612
  "scope": "device",
606
613
  "local": true
607
614
  },
615
+ "contextEditorVisible": {
616
+ "scope": "device",
617
+ "local": true
618
+ },
608
619
  "contextEditorTreeWidth": {
609
620
  "scope": "device",
610
621
  "local": true
@@ -617,6 +628,10 @@
617
628
  "scope": "device",
618
629
  "local": true
619
630
  },
631
+ "messageQueueExpanded": {
632
+ "scope": "device",
633
+ "local": true
634
+ },
620
635
  "workStatusScrollTop": {
621
636
  "scope": "device",
622
637
  "local": true
@@ -108,6 +108,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
108
108
  console.log('HTTP server closed');
109
109
  resolve();
110
110
  });
111
+ // The backend has stopped. Active SSE/HTTP clients must not keep
112
+ // Desktop waiting for the outer shutdown deadline.
113
+ server.closeAllConnections?.();
111
114
  }),
112
115
  new Promise((resolve) => {
113
116
  closeTimeout = setTimeout(() => {
@@ -1,4 +1,5 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import http from 'node:http';
2
3
 
3
4
  import { createGracefulShutdownRuntime } from './shutdown-runtime.js';
4
5
 
@@ -55,4 +56,27 @@ describe('graceful shutdown runtime', () => {
55
56
  expect(warnSpy).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
56
57
  expect(vi.getTimerCount()).toBe(0);
57
58
  });
59
+
60
+ it('closes an active HTTP stream instead of waiting for the shutdown deadline', async () => {
61
+ const server = http.createServer((_request, response) => {
62
+ response.writeHead(200, { 'content-type': 'text/event-stream' });
63
+ response.write('data: fixture\n\n');
64
+ });
65
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
66
+ const request = http.get(`http://127.0.0.1:${server.address().port}`);
67
+ request.on('error', () => {});
68
+ const response = await new Promise((resolve) => request.once('response', resolve));
69
+ response.resume();
70
+ const closed = new Promise((resolve) => response.once('close', resolve));
71
+ const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
72
+ try {
73
+ await createRuntime(server).gracefulShutdown({ exitProcess: false });
74
+ expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
75
+ await closed;
76
+ } finally {
77
+ request.destroy();
78
+ server.closeAllConnections();
79
+ await new Promise((resolve) => server.close(resolve));
80
+ }
81
+ });
58
82
  });
@@ -0,0 +1,53 @@
1
+ import { inflateRaw } from 'node:zlib';
2
+ import { promisify } from 'node:util';
3
+
4
+ const inflate = promisify(inflateRaw);
5
+ const MAX_FILE_BYTES = 512 * 1024;
6
+
7
+ // Read only selected JSON entries in memory. Nothing from a VSIX is executed or
8
+ // extracted onto the host filesystem.
9
+ export function openThemeArchive(bytes) {
10
+ let end = bytes.length - 22;
11
+ const lower = Math.max(0, end - 65535);
12
+ while (end >= lower && (bytes.readUInt32LE(end) !== 0x06054b50 || end + 22 + bytes.readUInt16LE(end + 20) !== bytes.length)) end--;
13
+ if (end < lower) throw new Error('Invalid VSIX directory');
14
+ const count = bytes.readUInt16LE(end + 10);
15
+ const directorySize = bytes.readUInt32LE(end + 12);
16
+ let cursor = bytes.readUInt32LE(end + 16);
17
+ if (count > 5000 || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6)
18
+ || bytes.readUInt16LE(end + 8) !== count || cursor + directorySize !== end) throw new Error('Unsupported VSIX directory');
19
+ const entries = new Map();
20
+ let totalSize = 0;
21
+ for (let index = 0; index < count; index++) {
22
+ if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) throw new Error('Invalid VSIX entry');
23
+ const flags = bytes.readUInt16LE(cursor + 8);
24
+ const method = bytes.readUInt16LE(cursor + 10);
25
+ const compressed = bytes.readUInt32LE(cursor + 20);
26
+ const size = bytes.readUInt32LE(cursor + 24);
27
+ const nameLength = bytes.readUInt16LE(cursor + 28);
28
+ const next = cursor + 46 + nameLength + bytes.readUInt16LE(cursor + 30) + bytes.readUInt16LE(cursor + 32);
29
+ const offset = bytes.readUInt32LE(cursor + 42);
30
+ if (next > end || nameLength > 1024 || flags & 1 || ![0, 8].includes(method)) throw new Error('Unsupported VSIX entry');
31
+ const name = bytes.toString('utf8', cursor + 46, cursor + 46 + nameLength);
32
+ const parts = name.replace(/\/$/, '').split('/');
33
+ if (parts.some((part) => !part || part === '.' || part === '..') || /[\\\0:]/.test(name) || entries.has(name)) throw new Error('Unsafe VSIX path');
34
+ totalSize += size;
35
+ if (totalSize > 100 * 1024 * 1024) throw new Error('VSIX contents too large');
36
+ entries.set(name, { offset, compressed, size, method });
37
+ cursor = next;
38
+ }
39
+ if (cursor !== end) throw new Error('Invalid VSIX directory size');
40
+ return async (name) => {
41
+ const entry = entries.get(name);
42
+ if (!entry || entry.size > MAX_FILE_BYTES) throw new Error('Theme file missing or too large');
43
+ const { offset, compressed, size, method } = entry;
44
+ if (offset + 30 > bytes.length || bytes.readUInt32LE(offset) !== 0x04034b50 || bytes.readUInt16LE(offset + 8) !== method) throw new Error('Invalid VSIX file');
45
+ const nameLength = bytes.readUInt16LE(offset + 26);
46
+ const start = offset + 30 + nameLength + bytes.readUInt16LE(offset + 28);
47
+ if (start + compressed > bytes.length || bytes.toString('utf8', offset + 30, offset + 30 + nameLength) !== name) throw new Error('Invalid VSIX file bounds');
48
+ const data = bytes.subarray(start, start + compressed);
49
+ const result = method === 0 ? data : await inflate(data, { maxOutputLength: MAX_FILE_BYTES });
50
+ if (result.length !== size) throw new Error('Invalid VSIX file size');
51
+ return result.toString('utf8');
52
+ };
53
+ }
@@ -0,0 +1,171 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { parse } from 'jsonc-parser';
4
+ import { z } from 'zod';
5
+ import { openThemeArchive } from './theme-archive.js';
6
+
7
+ const identifier = z.string().min(1).max(160).regex(/^[\w-]+$/);
8
+ const version = z.string().min(1).max(80).regex(/^[\w.+-]+$/);
9
+ const settings = z.object({ foreground: z.string().optional() });
10
+ const tokenRules = z.array(z.object({ scope: z.union([z.string(), z.array(z.string())]).optional(), settings }));
11
+ const sourceSchema = z.object({
12
+ name: z.string().optional(), type: z.string().optional(), include: z.string().optional(),
13
+ colors: z.record(z.string(), z.string().nullable()).default({}),
14
+ tokenColors: z.union([z.string(), tokenRules]).optional(),
15
+ semanticHighlighting: z.boolean().optional(),
16
+ semanticTokenColors: z.record(z.string(), z.union([z.string(), settings])).optional(),
17
+ });
18
+ const extensionSchema = z.object({
19
+ namespace: identifier, name: identifier, version,
20
+ displayName: z.string().optional(), files: z.object({ icon: z.string().optional() }),
21
+ });
22
+ const detailSchema = extensionSchema.extend({ files: z.object({ download: z.string(), sha256: z.string() }) });
23
+ const manifestSchema = z.object({
24
+ publisher: identifier, name: identifier, version,
25
+ contributes: z.object({ themes: z.array(z.object({
26
+ label: z.string().min(1).max(160), path: z.string().min(1).max(1024),
27
+ uiTheme: z.enum(['vs', 'vs-dark', 'hc-black', 'hc-light']).optional(),
28
+ })).min(1).max(40) }),
29
+ });
30
+ const querySchema = z.object({ query: z.string().trim().min(1).max(160) });
31
+ const packageSchema = z.object({ namespace: identifier, name: identifier, version });
32
+ const hosts = new Set(['open-vsx.org', 'openvsx.eclipsecontent.org']);
33
+
34
+ function trustedUrl(value) {
35
+ const url = new URL(value);
36
+ if (url.protocol !== 'https:' || !hosts.has(url.hostname) || url.port || url.username || url.password) throw new Error('Unexpected catalog host');
37
+ return url;
38
+ }
39
+
40
+ async function download(url, maxBytes, signal, fetchImpl) {
41
+ for (let redirects = 0; redirects < 4; redirects++) {
42
+ const response = await fetchImpl(trustedUrl(url), { signal, redirect: 'manual' });
43
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
44
+ await response.body?.cancel();
45
+ url = new URL(response.headers.get('location') ?? '', url).href;
46
+ continue;
47
+ }
48
+ if (!response.ok || !response.body || Number(response.headers.get('content-length')) > maxBytes) {
49
+ await response.body?.cancel();
50
+ throw new Error('Catalog request failed');
51
+ }
52
+ const reader = response.body.getReader();
53
+ const chunks = [];
54
+ let length = 0;
55
+ try {
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+ length += value.byteLength;
60
+ if (length > maxBytes) throw new Error('Catalog response too large');
61
+ chunks.push(value);
62
+ }
63
+ } finally {
64
+ await reader.cancel();
65
+ reader.releaseLock();
66
+ }
67
+ return Buffer.concat(chunks, length);
68
+ }
69
+ throw new Error('Too many catalog redirects');
70
+ }
71
+
72
+ function parseJsonc(text, schema) {
73
+ const errors = [];
74
+ const value = parse(text, errors, { allowTrailingComma: true });
75
+ if (errors.length) throw new Error('Invalid theme JSON');
76
+ return schema.parse(value);
77
+ }
78
+
79
+ function themePath(from, reference) {
80
+ if (/[\\\0:]/.test(reference) || reference.startsWith('/')) throw new Error('Invalid theme reference');
81
+ const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(from), reference));
82
+ if (!resolved.startsWith('extension/')) throw new Error('Theme reference escapes package');
83
+ return resolved;
84
+ }
85
+
86
+ export function createThemeCatalog({ fetchImpl = fetch } = {}) {
87
+ const search = async (input, signal = AbortSignal.timeout(15000)) => {
88
+ const { query } = querySchema.parse(input);
89
+ const url = new URL('https://open-vsx.org/api/-/search');
90
+ url.search = new URLSearchParams({ query, category: 'Themes', size: '24', sortBy: 'relevance' }).toString();
91
+ const bytes = await download(url.href, 1024 * 1024, signal, fetchImpl);
92
+ const result = z.object({ extensions: z.array(extensionSchema).max(24) }).parse(JSON.parse(bytes.toString('utf8')));
93
+ return result.extensions.map((extension) => ({
94
+ namespace: extension.namespace, name: extension.name, version: extension.version,
95
+ label: extension.displayName || extension.name,
96
+ icon: extension.files.icon && URL.canParse(extension.files.icon) && hosts.has(new URL(extension.files.icon).hostname)
97
+ ? trustedUrl(extension.files.icon).href : null,
98
+ }));
99
+ };
100
+
101
+ const readPackage = async (input, signal = AbortSignal.timeout(45000)) => {
102
+ const requested = packageSchema.parse(input);
103
+ const url = `https://open-vsx.org/api/${requested.namespace}/${requested.name}/${requested.version}`;
104
+ const detail = detailSchema.parse(JSON.parse((await download(url, 512 * 1024, signal, fetchImpl)).toString('utf8')));
105
+ const bytes = await download(detail.files.download, 20 * 1024 * 1024, signal, fetchImpl);
106
+ const checksum = (await download(detail.files.sha256, 256, signal, fetchImpl)).toString('utf8').trim().split(/\s+/)[0];
107
+ if (createHash('sha256').update(bytes).digest('hex') !== checksum?.toLowerCase()) throw new Error('VSIX checksum mismatch');
108
+ const read = openThemeArchive(bytes);
109
+ const manifest = parseJsonc(await read('extension/package.json'), manifestSchema);
110
+ if (manifest.publisher.toLowerCase() !== requested.namespace.toLowerCase() || manifest.name !== requested.name || manifest.version !== requested.version) throw new Error('VSIX identity mismatch');
111
+ let reads = 0;
112
+ const load = async (file, ancestors = []) => {
113
+ signal.throwIfAborted();
114
+ if (ancestors.includes(file) || ancestors.length >= 8 || ++reads > 320) throw new Error('Too many theme references');
115
+ const source = parseJsonc(await read(file), sourceSchema);
116
+ const parent = source.include ? await load(themePath(file, source.include), [...ancestors, file]) : null;
117
+ const reference = z.string().safeParse(source.tokenColors);
118
+ const rules = reference.success
119
+ ? parseJsonc(await read(themePath(file, reference.data)), tokenRules) : tokenRules.parse(source.tokenColors ?? []);
120
+ const { include: _include, ...own } = source;
121
+ return {
122
+ ...parent, ...own,
123
+ colors: { ...parent?.colors, ...source.colors },
124
+ tokenColors: [...(parent?.tokenColors ?? []), ...(rules ?? [])],
125
+ semanticTokenColors: { ...parent?.semanticTokenColors, ...source.semanticTokenColors },
126
+ };
127
+ };
128
+ const themes = [];
129
+ let totalBytes = 0;
130
+ for (const contribution of manifest.contributes.themes) {
131
+ try {
132
+ const file = themePath('extension/package.json', contribution.path);
133
+ const source = await load(file);
134
+ source.name = contribution.label;
135
+ if (contribution.uiTheme) source.type = contribution.uiTheme === 'vs' ? 'light' : contribution.uiTheme === 'vs-dark' ? 'dark' : contribution.uiTheme;
136
+ const text = JSON.stringify(source);
137
+ totalBytes += Buffer.byteLength(text);
138
+ if (Buffer.byteLength(text) > 512 * 1024 || totalBytes > 10 * 1024 * 1024) throw new Error('Resolved themes too large');
139
+ themes.push({ path: contribution.path, name: contribution.label, text, error: false });
140
+ } catch {
141
+ signal.throwIfAborted();
142
+ themes.push({ path: contribution.path, name: contribution.label, text: '', error: true });
143
+ }
144
+ }
145
+ return themes;
146
+ };
147
+ return { search, readPackage };
148
+ }
149
+
150
+ export function registerThemeCatalogRoutes(app, catalog = createThemeCatalog()) {
151
+ for (const [route, operation, schema] of [
152
+ ['/api/config/themes/catalog/search', catalog.search, querySchema],
153
+ ['/api/config/themes/catalog/package', catalog.readPackage, packageSchema],
154
+ ]) {
155
+ app.post(route, async (req, res) => {
156
+ const input = schema.safeParse(req.body);
157
+ if (!input.success) return res.status(400).json({ error: 'invalid' });
158
+ const controller = new AbortController();
159
+ const cancel = () => controller.abort();
160
+ res.on('close', cancel);
161
+ try {
162
+ const items = await operation(input.data, AbortSignal.any([controller.signal, AbortSignal.timeout(45000)]));
163
+ res.json({ items });
164
+ } catch {
165
+ if (!res.destroyed) res.status(502).json({ error: 'catalog' });
166
+ } finally {
167
+ res.off('close', cancel);
168
+ }
169
+ });
170
+ }
171
+ }
@@ -0,0 +1,125 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createHash } from 'node:crypto';
3
+ import { deflateRawSync } from 'node:zlib';
4
+ import { createThemeCatalog, registerThemeCatalogRoutes } from './theme-catalog.js';
5
+ import { registerCommonRequestMiddleware } from './core-routes.js';
6
+ import express from 'express';
7
+ import request from 'supertest';
8
+ import { openThemeArchive } from './theme-archive.js';
9
+
10
+ function archive(files) {
11
+ const locals = [];
12
+ const directory = [];
13
+ let offset = 0;
14
+ for (const [filename, content] of Object.entries(files)) {
15
+ const name = Buffer.from(filename);
16
+ const data = Buffer.from(content);
17
+ const compressed = deflateRawSync(data);
18
+ const header = Buffer.alloc(30);
19
+ header.writeUInt32LE(0x04034b50);
20
+ header.writeUInt16LE(8, 8);
21
+ header.writeUInt32LE(compressed.length, 18);
22
+ header.writeUInt32LE(data.length, 22);
23
+ header.writeUInt16LE(name.length, 26);
24
+ locals.push(header, name, compressed);
25
+ const central = Buffer.alloc(46);
26
+ central.writeUInt32LE(0x02014b50);
27
+ central.writeUInt16LE(8, 10);
28
+ central.writeUInt32LE(compressed.length, 20);
29
+ central.writeUInt32LE(data.length, 24);
30
+ central.writeUInt16LE(name.length, 28);
31
+ central.writeUInt32LE(offset, 42);
32
+ directory.push(central, name);
33
+ offset += header.length + name.length + compressed.length;
34
+ }
35
+ const end = Buffer.alloc(22);
36
+ end.writeUInt32LE(0x06054b50);
37
+ end.writeUInt16LE(directory.length / 2, 8);
38
+ end.writeUInt16LE(directory.length / 2, 10);
39
+ end.writeUInt32LE(Buffer.concat(directory).length, 12);
40
+ end.writeUInt32LE(offset, 16);
41
+ return Buffer.concat([...locals, ...directory, end]);
42
+ }
43
+
44
+ const identity = { namespace: 'Author', name: 'fixture', version: '1.0.0' };
45
+ const manifest = { publisher: 'Author', name: 'fixture', version: '1.0.0', contributes: { themes: [
46
+ { label: 'Readable Night', path: './themes/night.json', uiTheme: 'vs-dark' },
47
+ { label: 'Broken sibling', path: '../../outside.json', uiTheme: 'vs' },
48
+ ] } };
49
+ const packageFiles = {
50
+ 'extension/package.json': JSON.stringify(manifest),
51
+ 'extension/base.json': '{"colors":{"editor.background":"#111111","editor.foreground":"#eeeeee"},"tokenColors":[{"scope":"string","settings":{"foreground":"#aaaaaa"}}]}',
52
+ 'extension/themes/night.json': '{ // JSONC\n"include":"../base.json", "colors":{"editor.foreground":"#ffffff"},"tokenColors":"./tokens.json",}',
53
+ 'extension/themes/tokens.json': '[{"scope":"keyword","settings":{"foreground":"#aabbcc"}}]',
54
+ };
55
+
56
+ function transport(bytes, overrides = {}) {
57
+ const responses = new Map([
58
+ ['https://open-vsx.org/api/Author/fixture/1.0.0', JSON.stringify({ ...identity, files: { download: 'https://open-vsx.org/package.vsix', sha256: 'https://open-vsx.org/package.sha256' } })],
59
+ ['https://open-vsx.org/package.vsix', bytes],
60
+ ['https://open-vsx.org/package.sha256', createHash('sha256').update(bytes).digest('hex')],
61
+ ...Object.entries(overrides),
62
+ ]);
63
+ return async (url) => {
64
+ const body = responses.get(url.href);
65
+ if (body === undefined) throw new Error(`Unexpected request ${url}`);
66
+ return new Response(body);
67
+ };
68
+ }
69
+
70
+ describe('Open VSX theme catalog', () => {
71
+ test('catalog requests retain their bodies through the production middleware', async () => {
72
+ const app = express();
73
+ registerCommonRequestMiddleware(app, { express });
74
+ const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
75
+ registerThemeCatalogRoutes(app, {
76
+ search: async (input) => { expect(input).toEqual({ query: 'nord' }); return []; },
77
+ readPackage: catalog.readPackage,
78
+ });
79
+ await request(app).post('/api/config/themes/catalog/search').send({ query: 'nord' }).expect(200);
80
+ const response = await request(app).post('/api/config/themes/catalog/package').send(identity).expect(200);
81
+ expect(response.body.items[0].name).toBe('Readable Night');
82
+ });
83
+ test('resolves JSONC includes and external token files, preserving healthy siblings and manifest labels', async () => {
84
+ const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
85
+ const result = await catalog.readPackage(identity);
86
+ expect(result[0].error).toBe(false);
87
+ const source = JSON.parse(result[0].text);
88
+ expect(source.name).toBe('Readable Night');
89
+ expect(source.type).toBe('dark');
90
+ expect(source.colors).toEqual({ 'editor.background': '#111111', 'editor.foreground': '#ffffff' });
91
+ expect(source.tokenColors.map((rule) => rule.scope)).toEqual(['string', 'keyword']);
92
+ expect(source.include).toBeUndefined();
93
+ expect(result[1].error).toBe(true);
94
+ });
95
+
96
+ test('rejects checksum and package identity mismatches', async () => {
97
+ const bytes = archive(packageFiles);
98
+ await expect(createThemeCatalog({ fetchImpl: transport(bytes, { 'https://open-vsx.org/package.sha256': '0'.repeat(64) }) }).readPackage(identity)).rejects.toThrow('checksum');
99
+ const wrong = archive({ ...packageFiles, 'extension/package.json': JSON.stringify({ ...manifest, publisher: 'Impostor' }) });
100
+ await expect(createThemeCatalog({ fetchImpl: transport(wrong) }).readPackage(identity)).rejects.toThrow('identity');
101
+ });
102
+
103
+ test('limits decompression and rejects traversal and cyclic includes', async () => {
104
+ expect(() => openThemeArchive(archive({ '../outside.json': '{}' }))).toThrow('Unsafe');
105
+ const read = openThemeArchive(archive({ 'extension/huge.json': ' '.repeat(512 * 1024 + 1) }));
106
+ await expect(read('extension/huge.json')).rejects.toThrow('large');
107
+ const cyclic = archive({ ...packageFiles, 'extension/base.json': '{"include":"./themes/night.json"}' });
108
+ expect((await createThemeCatalog({ fetchImpl: transport(cyclic) }).readPackage(identity))[0].error).toBe(true);
109
+ });
110
+
111
+ test('searches only the theme category and rejects redirects outside the catalog', async () => {
112
+ const catalog = createThemeCatalog({ fetchImpl: async (url, options) => {
113
+ expect(url.searchParams.get('category')).toBe('Themes');
114
+ expect(url.searchParams.get('query')).toBe('Dune');
115
+ expect(options.redirect).toBe('manual');
116
+ return Response.json({ extensions: [{ ...identity, displayName: 'Dune', files: {} }] });
117
+ } });
118
+ expect((await catalog.search({ query: 'Dune' }))[0].label).toBe('Dune');
119
+ let calls = 0;
120
+ const hostile = createThemeCatalog({ fetchImpl: async () => { calls++; return new Response(null, { status: 302, headers: { location: 'http://127.0.0.1/private' } }); } });
121
+ await expect(hostile.search({ query: 'Dune' })).rejects.toThrow('host');
122
+ expect(calls).toBe(1);
123
+ await expect(catalog.readPackage({ ...identity, name: '../secret' })).rejects.toThrow();
124
+ });
125
+ });