@vantran-se/openchamber-web 1.23.1 → 1.24.3

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-BjJHrzmU.js} +1 -1
  5. package/dist/assets/DiffView-COVQhWGd.js +4 -0
  6. package/dist/assets/FilesView-B1V5JxFX.js +13 -0
  7. package/dist/assets/GitView-CDOJy5Ay.js +6 -0
  8. package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
  9. package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-D0C9nU7K.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-D7FKg1XJ.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
  72. package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
  73. package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DnEhjXcC.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
  75. package/dist/assets/PlanView-LW2z_EAP.js +5 -0
  76. package/dist/assets/PluginPane-B4tKno2h.js +3 -0
  77. package/dist/assets/SettingsView-DEaMulN9.js +29 -0
  78. package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-FLDJXOgQ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
  80. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-0pyr_mHY.js} +3 -3
  83. package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-yNj4nOP5.js} +2 -2
  84. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
  85. package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DxTBMuu-.js} +1 -1
  87. package/dist/assets/de-DgxgFyGG.js +10 -0
  88. package/dist/assets/es-BFsZj5F_.js +16 -0
  89. package/dist/assets/fr-jdZ5KKQi.js +14 -0
  90. package/dist/assets/{index-eo1-D_7z.js → index-CKHjPKbq.js} +1 -1
  91. package/dist/assets/index-Cbci4i1W.js +40 -0
  92. package/dist/assets/index-DqaMWNrC.css +1 -0
  93. package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-D7FkBMP6.js} +1 -1
  94. package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-CL2vsrIZ.js} +1 -1
  95. package/dist/assets/ja-C5kKrhO4.js +16 -0
  96. package/dist/assets/ko-DXDTT_0O.js +16 -0
  97. package/dist/assets/main-CZALtVek.js +896 -0
  98. package/dist/assets/main-CgXMBNvG.js +2 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
  101. package/dist/assets/miniChat-CwTmDixi.js +2 -0
  102. package/dist/assets/mobile-CLf2vCtO.js +2 -0
  103. package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-Bsba8KFp.js} +3 -3
  104. package/dist/assets/pl-D-7x4Y8Q.js +16 -0
  105. package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
  107. package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
  109. package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-DzjbLd8P.js} +1 -1
  110. package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
  111. package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
  112. package/dist/assets/tr-Dr-oJ22L.js +7 -0
  113. package/dist/assets/uk-9Nx83UcI.js +16 -0
  114. package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
  115. package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
  116. package/dist/assets/useRootScrollLock-DdvHf9Pt.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-CdzUmUyZ.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-B8IcVQAA.js} +2 -2
  128. package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
  129. package/dist/assets/zh-TW-0BB4gpHM.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
@@ -0,0 +1,144 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import express from 'express';
6
+ import request from 'supertest';
7
+ import { createThemeRuntime } from './theme-runtime.js';
8
+ import { registerSettingsUtilityRoutes, registerCommonRequestMiddleware } from './core-routes.js';
9
+
10
+ const directories = [];
11
+ afterEach(async () => {
12
+ await Promise.all(directories.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true })));
13
+ });
14
+
15
+ const definition = {
16
+ metadata: { id: '../../escape', name: 'Imported fixture', variant: 'dark' },
17
+ colors: {
18
+ primary: { base: '#ffcc66' },
19
+ surface: { background: '#111111', foreground: '#eeeeee', muted: '#181818', mutedForeground: '#999999', elevated: '#222222' },
20
+ interactive: { border: '#333333' },
21
+ status: { error: '#ff6666', warning: '#ffcc66', success: '#66cc66', info: '#66aaff' },
22
+ syntax: { base: { comment: '#888888', keyword: '#aa88ff', string: '#88cc66', number: '#ffcc66', function: '#66aaff', variable: '#eeeeee', type: '#66dddd', operator: '#ff6677' } },
23
+ },
24
+ };
25
+
26
+ async function setup(overrides = {}) {
27
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-theme-import-'));
28
+ directories.push(root);
29
+ const themesDir = path.join(root, 'themes');
30
+ const runtime = createThemeRuntime({ fsPromises: { ...fs, ...overrides }, path, themesDir, maxThemeJsonBytes: 512 * 1024, logger: console });
31
+ const app = express();
32
+ registerCommonRequestMiddleware(app, { express });
33
+ registerSettingsUtilityRoutes(app, runtime);
34
+ return { root, themesDir, runtime, app };
35
+ }
36
+
37
+ describe('theme import persistence', () => {
38
+ it('deletes only imported files, leaves siblings intact and allows an identical reimport', async () => {
39
+ const { app, runtime, themesDir } = await setup();
40
+ const theme = await runtime.saveImportedTheme(definition);
41
+ await fs.writeFile(path.join(themesDir, 'custom.json'), JSON.stringify(definition));
42
+ await request(app).delete('/api/config/themes/openchamber-dark').expect(200);
43
+ await request(app).delete(`/api/config/themes/${theme.metadata.id}`).expect(200);
44
+ expect(await fs.readdir(themesDir)).toEqual(['custom.json']);
45
+ await request(app).delete(`/api/config/themes/${theme.metadata.id}`).expect(200);
46
+ expect(await runtime.saveImportedTheme(definition)).toEqual(theme);
47
+ });
48
+
49
+ it('does not follow a symlink during deletion or report deletion failure as success', async () => {
50
+ const { app, root, themesDir } = await setup();
51
+ await fs.mkdir(themesDir);
52
+ const outside = path.join(root, 'outside.json');
53
+ await fs.writeFile(outside, 'keep');
54
+ const id = `imported-vscode-${'a'.repeat(24)}`;
55
+ await fs.symlink(outside, path.join(themesDir, `${id}.json`));
56
+ await request(app).delete(`/api/config/themes/${id}`).expect(200);
57
+ expect(await fs.readFile(outside, 'utf8')).toBe('keep');
58
+ const denied = await setup({ unlink: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
59
+ await fs.mkdir(denied.themesDir);
60
+ await fs.writeFile(path.join(denied.themesDir, `${id}.json`), JSON.stringify({ ...definition, metadata: { ...definition.metadata, id } }));
61
+ await request(denied.app).delete(`/api/config/themes/${id}`).expect(500);
62
+ expect(await fs.readdir(denied.themesDir)).toEqual([`${id}.json`]);
63
+ });
64
+ it('deletes a hand-added theme by its metadata ID, not by an assumed filename', async () => {
65
+ const { app, themesDir } = await setup();
66
+ await fs.mkdir(themesDir);
67
+ const custom = { ...definition, metadata: { ...definition.metadata, id: 't3-code-dark', name: 'T3 Code' } };
68
+ await fs.writeFile(path.join(themesDir, 'my-custom-palette.json'), JSON.stringify(custom));
69
+ const loaded = await request(app).get('/api/config/themes').expect(200);
70
+ expect(loaded.body.themes[0].metadata.id).toBe('t3-code-dark');
71
+ await request(app).delete('/api/config/themes/t3-code-dark').expect(200);
72
+ expect(await fs.readdir(themesDir)).toEqual([]);
73
+ });
74
+
75
+ it('refuses ambiguous duplicate IDs and does not turn an ID into a filesystem path', async () => {
76
+ const { app, themesDir, root } = await setup();
77
+ await fs.mkdir(themesDir);
78
+ const custom = { ...definition, metadata: { ...definition.metadata, id: 'duplicate' } };
79
+ for (const name of ['one.json', 'two.json']) await fs.writeFile(path.join(themesDir, name), JSON.stringify(custom));
80
+ await request(app).delete('/api/config/themes/duplicate').expect(409);
81
+ expect((await fs.readdir(themesDir)).sort()).toEqual(['one.json', 'two.json']);
82
+ await fs.writeFile(path.join(root, 'outside.json'), JSON.stringify(custom));
83
+ await request(app).delete('/api/config/themes/..%2Foutside').expect(200);
84
+ expect(await fs.readFile(path.join(root, 'outside.json'), 'utf8')).toBe(JSON.stringify(custom));
85
+ });
86
+
87
+ it('allows deleting a readable theme beside an unreadable sibling, but does not treat failed reads as absence', async () => {
88
+ const { runtime, themesDir } = await setup({ readFile: async (file, ...args) => {
89
+ if (path.basename(file) === 'unreadable.json') throw Object.assign(new Error('denied'), { code: 'EACCES' });
90
+ return fs.readFile(file, ...args);
91
+ } });
92
+ const theme = await runtime.saveImportedTheme(definition);
93
+ await fs.writeFile(path.join(themesDir, 'unreadable.json'), JSON.stringify(definition));
94
+ await runtime.deleteImportedTheme(theme.metadata.id);
95
+ expect(await fs.readdir(themesDir)).toEqual(['unreadable.json']);
96
+ await expect(runtime.deleteImportedTheme('possibly-unreadable')).rejects.toMatchObject({ code: 'EACCES' });
97
+ });
98
+ it('saves through the API under a server-owned ID and returns it through reload', async () => {
99
+ const { app, themesDir, root } = await setup();
100
+ const response = await request(app).post('/api/config/themes').send({ theme: definition }).expect(201);
101
+ expect(response.body.theme.metadata.id).toMatch(/^imported-vscode-[a-f0-9]{24}$/);
102
+ expect(await fs.readdir(root)).toEqual(['themes']);
103
+ expect(await fs.readdir(themesDir)).toEqual([`${response.body.theme.metadata.id}.json`]);
104
+ const loaded = await request(app).get('/api/config/themes').expect(200);
105
+ expect(loaded.body.themes).toEqual([response.body.theme]);
106
+ });
107
+
108
+ it('deduplicates simultaneous imports and never overwrites a subsequently edited theme', async () => {
109
+ const { runtime, themesDir } = await setup();
110
+ const [first, second] = await Promise.all([runtime.saveImportedTheme(definition), runtime.saveImportedTheme(definition)]);
111
+ expect(first).toEqual(second);
112
+ expect((await fs.readdir(themesDir)).length).toBe(1);
113
+ const filename = path.join(themesDir, `${first.metadata.id}.json`);
114
+ await fs.writeFile(filename, 'manually edited');
115
+ await expect(runtime.saveImportedTheme(definition)).rejects.toMatchObject({ code: 'conflict', status: 409 });
116
+ expect(await fs.readFile(filename, 'utf8')).toBe('manually edited');
117
+ expect(await fs.readdir(themesDir)).toEqual([`${first.metadata.id}.json`]);
118
+ });
119
+
120
+ it('rejects malformed definitions and unsafe token names without creating files', async () => {
121
+ const { app, root } = await setup();
122
+ await request(app).post('/api/config/themes').send({ theme: { ...definition, colors: {} } }).expect(400);
123
+ await request(app).post('/api/config/themes').send({ theme: { ...definition, colors: { ...definition.colors, syntax: { ...definition.colors.syntax, tokens: { 'x;body': '#000000' } } } } }).expect(400);
124
+ expect(await fs.readdir(root)).toEqual([]);
125
+ });
126
+
127
+ it('cleans temporary files after a publication failure', async () => {
128
+ const { runtime, themesDir } = await setup({ link: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
129
+ await expect(runtime.saveImportedTheme(definition)).rejects.toMatchObject({ code: 'EACCES' });
130
+ expect(await fs.readdir(themesDir)).toEqual([]);
131
+ });
132
+
133
+ it('does not report a directory read failure as an empty theme library', async () => {
134
+ const { runtime } = await setup({ readdir: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
135
+ await expect(runtime.readCustomThemesFromDisk()).rejects.toMatchObject({ code: 'EACCES' });
136
+ });
137
+
138
+ it('rejects oversized imports without touching the themes directory', async () => {
139
+ const { runtime, root } = await setup();
140
+ const tokens = Object.fromEntries(Array.from({ length: 30000 }, (_, index) => [`token${index}`, '#123456']));
141
+ await expect(runtime.saveImportedTheme({ ...definition, colors: { ...definition.colors, syntax: { ...definition.colors.syntax, tokens } } })).rejects.toMatchObject({ code: 'size', status: 413 });
142
+ expect(await fs.readdir(root)).toEqual([]);
143
+ });
144
+ });
@@ -1,3 +1,29 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+
4
+ const color = z.string().regex(/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i);
5
+ const role = z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/).max(64);
6
+ const importedThemeSchema = z.object({
7
+ metadata: z.object({
8
+ name: z.string().trim().min(1).max(160),
9
+ author: z.string().max(160).optional(),
10
+ variant: z.enum(['light', 'dark']),
11
+ description: z.string().max(1024).default(''),
12
+ version: z.string().max(40).default('1.0.0'),
13
+ tags: z.array(z.string().max(40)).max(16).default([]),
14
+ }),
15
+ colors: z.record(role, z.record(role, z.union([color, z.record(role, color)]))),
16
+ });
17
+ const fileErrorSchema = z.object({ code: z.string() });
18
+
19
+ export class ThemeImportStorageError extends Error {
20
+ constructor(code, status) {
21
+ super(code);
22
+ this.code = code;
23
+ this.status = status;
24
+ }
25
+ }
26
+
1
27
  export const createThemeRuntime = (dependencies) => {
2
28
  const {
3
29
  fsPromises,
@@ -34,46 +60,24 @@ export const createThemeRuntime = (dependencies) => {
34
60
  const status = colors.status;
35
61
  const syntax = colors.syntax;
36
62
  const syntaxBase = syntax && typeof syntax === 'object' ? syntax.base : null;
37
- const syntaxHighlights = syntax && typeof syntax === 'object' ? syntax.highlights : null;
38
63
 
39
- if (!primary || !surface || !interactive || !status || !syntaxBase || !syntaxHighlights) {
64
+ if (!primary || !surface || !interactive || !status || !syntaxBase) {
40
65
  return null;
41
66
  }
42
67
 
43
- // Minimal fields required by CSSVariableGenerator and diff/syntax rendering.
68
+ // Authored inputs only. The UI resolves optional roles before rendering.
44
69
  const required = [
45
70
  primary.base,
46
- primary.foreground,
47
71
  surface.background,
48
72
  surface.foreground,
49
73
  surface.muted,
50
74
  surface.mutedForeground,
51
75
  surface.elevated,
52
- surface.elevatedForeground,
53
- surface.subtle,
54
76
  interactive.border,
55
- interactive.selection,
56
- interactive.selectionForeground,
57
- interactive.focusRing,
58
- interactive.hover,
59
77
  status.error,
60
- status.errorForeground,
61
- status.errorBackground,
62
- status.errorBorder,
63
78
  status.warning,
64
- status.warningForeground,
65
- status.warningBackground,
66
- status.warningBorder,
67
79
  status.success,
68
- status.successForeground,
69
- status.successBackground,
70
- status.successBorder,
71
80
  status.info,
72
- status.infoForeground,
73
- status.infoBackground,
74
- status.infoBorder,
75
- syntaxBase.background,
76
- syntaxBase.foreground,
77
81
  syntaxBase.keyword,
78
82
  syntaxBase.string,
79
83
  syntaxBase.number,
@@ -82,9 +86,6 @@ export const createThemeRuntime = (dependencies) => {
82
86
  syntaxBase.type,
83
87
  syntaxBase.comment,
84
88
  syntaxBase.operator,
85
- syntaxHighlights.diffAdded,
86
- syntaxHighlights.diffRemoved,
87
- syntaxHighlights.lineNumber,
88
89
  ];
89
90
 
90
91
  if (!required.every(isValidThemeColor)) {
@@ -156,12 +157,83 @@ export const createThemeRuntime = (dependencies) => {
156
157
  return [];
157
158
  }
158
159
  logger.warn('[themes] Failed to list custom themes dir:', error);
159
- return [];
160
+ throw error;
161
+ }
162
+ };
163
+
164
+ const saveImportedTheme = async (raw) => {
165
+ const parsed = importedThemeSchema.safeParse(raw);
166
+ if (!parsed.success) throw new ThemeImportStorageError('invalid', 400);
167
+ const normalized = normalizeThemeJson({ ...parsed.data, metadata: { ...parsed.data.metadata, id: 'import' } });
168
+ if (!normalized) throw new ThemeImportStorageError('invalid', 400);
169
+ // The client supplies colors, never a filename or an overwrite target.
170
+ // Identical imports have the same ID, including retries after a lost reply.
171
+ const digest = createHash('sha256').update(JSON.stringify(parsed.data)).digest('hex').slice(0, 24);
172
+ const theme = { ...normalized, metadata: { ...normalized.metadata, id: `imported-vscode-${digest}` } };
173
+ const contents = `${JSON.stringify(theme, null, 2)}\n`;
174
+ if (Buffer.byteLength(contents, 'utf8') > maxThemeJsonBytes) throw new ThemeImportStorageError('size', 413);
175
+ await fsPromises.mkdir(themesDir, { recursive: true });
176
+ const target = path.join(themesDir, `${theme.metadata.id}.json`);
177
+ const temporary = path.join(themesDir, `${randomUUID()}.tmp`);
178
+ try {
179
+ await fsPromises.writeFile(temporary, contents, { flag: 'wx', mode: 0o600 });
180
+ try {
181
+ // Link publishes the complete file atomically and cannot overwrite an
182
+ // existing theme, even if another import finishes at the same time.
183
+ await fsPromises.link(temporary, target);
184
+ } catch (error) {
185
+ const parsedError = fileErrorSchema.safeParse(error);
186
+ if (!parsedError.success || parsedError.data.code !== 'EEXIST') throw error;
187
+ if (await fsPromises.readFile(target, 'utf8') !== contents) throw new ThemeImportStorageError('conflict', 409);
188
+ }
189
+ return theme;
190
+ } finally {
191
+ await fsPromises.unlink(temporary).catch((error) => {
192
+ const parsedError = fileErrorSchema.safeParse(error);
193
+ if (!parsedError.success || parsedError.data.code !== 'ENOENT') logger.warn('[themes] Failed to clean import temporary file');
194
+ });
160
195
  }
161
196
  };
162
197
 
163
198
  return {
164
199
  normalizeThemeJson,
165
200
  readCustomThemesFromDisk,
201
+ saveImportedTheme,
202
+ async deleteImportedTheme(id) {
203
+ if (!z.string().trim().min(1).max(256).safeParse(id).success) throw new ThemeImportStorageError('invalid', 400);
204
+ const matches = [];
205
+ let readFailure;
206
+ let entries;
207
+ try { entries = await fsPromises.readdir(themesDir, { withFileTypes: true }); }
208
+ catch (error) {
209
+ if (fileErrorSchema.safeParse(error).data?.code === 'ENOENT') return;
210
+ throw error;
211
+ }
212
+ for (const entry of entries) {
213
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
214
+ const target = path.join(themesDir, entry.name);
215
+ let text;
216
+ try {
217
+ const stat = await fsPromises.lstat(target);
218
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxThemeJsonBytes) continue;
219
+ text = await fsPromises.readFile(target, 'utf8');
220
+ } catch (error) {
221
+ if (fileErrorSchema.safeParse(error).data?.code !== 'ENOENT') readFailure = error;
222
+ continue;
223
+ }
224
+ let theme;
225
+ try { theme = normalizeThemeJson(JSON.parse(text)); }
226
+ catch { continue; }
227
+ if (theme?.metadata.id === id) matches.push(target);
228
+ }
229
+ // IDs identify themes, not filenames. Refuse ambiguous matches rather
230
+ // than deleting an arbitrary sibling or constructing a path from the ID.
231
+ if (matches.length > 1) throw new ThemeImportStorageError('conflict', 409);
232
+ if (!matches.length) {
233
+ if (readFailure) throw readFailure;
234
+ return;
235
+ }
236
+ await fsPromises.unlink(matches[0]);
237
+ },
166
238
  };
167
239
  };
@@ -90,6 +90,21 @@ const createTestRuntime = ({ entries, files, stats }) => createThemeRuntime({
90
90
 
91
91
  describe('theme runtime', () => {
92
92
  describe('readCustomThemesFromDisk', () => {
93
+ it('loads compact authored roles without requiring derived colors', async () => {
94
+ const theme = validTheme('compact');
95
+ theme.colors.primary = { base: '#ffffff' };
96
+ theme.colors.interactive = { border: '#444444' };
97
+ theme.colors.status = { error: '#ff0000', warning: '#ffaa00', success: '#00ff00', info: '#0000ff' };
98
+ delete theme.colors.syntax.highlights;
99
+ delete theme.colors.syntax.base.background;
100
+ delete theme.colors.syntax.base.foreground;
101
+ const runtime = createTestRuntime({
102
+ entries: [fileEntry('compact.json'), fileEntry('bad.json')],
103
+ files: { '/themes/compact.json': JSON.stringify(theme), '/themes/bad.json': '{broken' },
104
+ stats: { '/themes/compact.json': { isFile: () => true, size: 1024 }, '/themes/bad.json': { isFile: () => true, size: 10 } },
105
+ });
106
+ expect((await runtime.readCustomThemesFromDisk()).map((item) => item.metadata.id)).toEqual(['compact']);
107
+ });
93
108
  it('loads valid theme files', async () => {
94
109
  const runtime = createTestRuntime({
95
110
  entries: [fileEntry('direct.json')],
@@ -16,6 +16,14 @@ Policy inheritance uses the nearest explicit session value. A child `false` ther
16
16
 
17
17
  Unknown lineage and failed policy loads fail closed. A failed pending-permission fetch is distinct from an empty successful response and never clears policy state.
18
18
 
19
+ ## Safety net
20
+
21
+ `evaluatePermission` (the routing runtime, `../routing/DOCUMENTATION.md`) is
22
+ consulted after the policy check and before the reply. `hold` counts the
23
+ request as handled without replying, so it stays on screen; `accept` replies as
24
+ before. A `permission.replied` event is passed to `onPermissionReplied` so the
25
+ routing runtime forgets its cached decision.
26
+
19
27
  ## Routes
20
28
 
21
29
  - `GET /api/permission-auto-accept`
@@ -25,6 +25,10 @@ export function createPermissionAutoAcceptRuntime({
25
25
  readSettingsFromDiskMigrated,
26
26
  persistSettings,
27
27
  broadcastGlobalUiEvent,
28
+ // The routing safety net: asked once per request before the automatic reply.
29
+ // `hold` leaves the request for the user; absent means every request is replied to.
30
+ evaluatePermission = null,
31
+ onPermissionReplied = null,
28
32
  fetchImpl = fetch,
29
33
  retryDelaysMs = RETRY_DELAYS_MS,
30
34
  requestTimeoutMs = REQUEST_TIMEOUT_MS,
@@ -148,6 +152,10 @@ export function createPermissionAutoAcceptRuntime({
148
152
  if (!permission?.id || !permission?.sessionID) return false;
149
153
  await load();
150
154
  if (!(await isSessionAutoAccepting(permission.sessionID, directory))) return false;
155
+ if (evaluatePermission) {
156
+ const verdict = await evaluatePermission(permission, directory);
157
+ if (verdict?.action === 'hold') return true;
158
+ }
151
159
  await request(`/permission/${encodeURIComponent(permission.id)}/reply`, {
152
160
  directory,
153
161
  method: 'POST',
@@ -218,6 +226,11 @@ export function createPermissionAutoAcceptRuntime({
218
226
  }
219
227
  if (payload?.type === 'permission.asked') {
220
228
  void processPermission(payload.properties, directory);
229
+ return;
230
+ }
231
+ if (payload?.type === 'permission.replied') {
232
+ const permissionId = payload.properties?.requestID;
233
+ if (typeof permissionId === 'string') onPermissionReplied?.(permissionId);
221
234
  }
222
235
  };
223
236
 
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { createPermissionAutoAcceptRuntime } from './runtime.js';
3
3
 
4
- const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
4
+ const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0], evaluatePermission, onPermissionReplied } = {}) => {
5
5
  let settings = stored ?? { permissionAutoAccept: { sessions: {} } };
6
6
  let eventHandler;
7
7
  let statusHandler;
@@ -16,6 +16,8 @@ const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
16
16
  persistSettings: async (changes) => { settings = { ...settings, ...changes }; },
17
17
  fetchImpl: fetchImpl ?? vi.fn(async () => new Response('[]')),
18
18
  retryDelaysMs,
19
+ evaluatePermission,
20
+ onPermissionReplied,
19
21
  });
20
22
  runtime.start();
21
23
  return {
@@ -143,4 +145,38 @@ describe('permission auto-accept runtime', () => {
143
145
  expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
144
146
  expect(await runtime.load()).toEqual({ sessions: { root: true }, revision: 1 });
145
147
  });
148
+
149
+ it('leaves a request held by the safety net unanswered and forgets it once replied', async () => {
150
+ const fetchImpl = vi.fn(async () => new Response('[]'));
151
+ const verdicts = { held: { action: 'hold', score: 0.9, kind: 'git_history' }, safe: { action: 'accept', score: 0.1 } };
152
+ const evaluatePermission = vi.fn(async (permission) => verdicts[permission.id]);
153
+ const onPermissionReplied = vi.fn();
154
+ const { runtime, emit } = createRuntime({
155
+ stored: { permissionAutoAccept: { sessions: { root: true } } },
156
+ fetchImpl,
157
+ evaluatePermission,
158
+ onPermissionReplied,
159
+ });
160
+ await runtime.load();
161
+
162
+ emit({ type: 'permission.asked', properties: { id: 'held', sessionID: 'root', permission: 'bash', metadata: {} } });
163
+ emit({ type: 'permission.asked', properties: { id: 'safe', sessionID: 'root', permission: 'bash', metadata: {} } });
164
+ await flush();
165
+
166
+ const replies = fetchImpl.mock.calls.map(([url]) => String(url)).filter((url) => url.includes('/reply'));
167
+ expect(replies).toEqual(['http://opencode.test/permission/safe/reply?directory=%2Fproject']);
168
+ expect(evaluatePermission).toHaveBeenCalledTimes(2);
169
+
170
+ emit({ type: 'permission.replied', properties: { sessionID: 'root', requestID: 'held', reply: 'once' } });
171
+ expect(onPermissionReplied).toHaveBeenCalledWith('held');
172
+ });
173
+
174
+ it('does not consult the safety net for sessions that are not auto-accepting', async () => {
175
+ const evaluatePermission = vi.fn(async () => ({ action: 'hold' }));
176
+ const { runtime, emit } = createRuntime({ evaluatePermission });
177
+ await runtime.load();
178
+ emit({ type: 'permission.asked', properties: { id: 'p', sessionID: 'manual', permission: 'bash', metadata: {} } });
179
+ await flush();
180
+ expect(evaluatePermission).not.toHaveBeenCalled();
181
+ });
146
182
  });
@@ -1,5 +1,5 @@
1
1
  import { DateTime, IANAZone } from 'luxon';
2
- import parser from 'cron-parser';
2
+ import { CronExpressionParser } from 'cron-parser';
3
3
 
4
4
  import { projectConfigFileStemOf, projectPathFromId } from './project-id.js';
5
5
  import {
@@ -145,7 +145,7 @@ const normalizeTimezone = (value, fallback = resolveDefaultTimezone()) => {
145
145
 
146
146
  const validateCronExpression = (expression, timezone) => {
147
147
  try {
148
- const iterator = parser.parseExpression(expression, {
148
+ const iterator = CronExpressionParser.parse(expression, {
149
149
  tz: timezone,
150
150
  currentDate: new Date(),
151
151
  });
@@ -27,7 +27,6 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
27
27
  | `codex` | Codex | `providers/codex.js` | `openai`, `codex`, `chatgpt` |
28
28
  | `command-code` | Command Code | `providers/command-code.js` | `command-code` OAuth/API credential in OpenCode `auth.json`, or `COMMAND_CODE_API_KEY` |
29
29
  | `cursor` | Cursor | `providers/cursor.js` | Environment/token files, OpenChamber-managed credentials, or explicit one-time Cursor import |
30
- | `crof` | CrofAI | `providers/crof.js` | `crof` (API key under `key` or `token`) |
31
30
  | `deepseek` | DeepSeek | `providers/deepseek.js` | `deepseek` (API key under `key` or `token`) |
32
31
  | `exe-dev` | exe.dev | `providers/exe-dev.js` | Usage API token stored under `~/.config/openchamber/quota/` |
33
32
  | `google` | Google | `providers/google/index.js` | `google`, `google.oauth`, Antigravity accounts file |
@@ -11,7 +11,6 @@ import * as claude from './claude/index.js';
11
11
  import * as clinePass from './cline-pass.js';
12
12
  import * as codex from './codex.js';
13
13
  import * as copilot from './copilot.js';
14
- import * as crof from './crof.js';
15
14
  import * as cursor from './cursor.js';
16
15
  import * as deepseek from './deepseek.js';
17
16
  import * as exeDev from './exe-dev.js';
@@ -50,12 +49,6 @@ const registry = {
50
49
  isConfigured: codex.isConfigured,
51
50
  fetchQuota: codex.fetchQuota
52
51
  },
53
- crof: {
54
- providerId: crof.providerId,
55
- providerName: crof.providerName,
56
- isConfigured: crof.isConfigured,
57
- fetchQuota: crof.fetchQuota
58
- },
59
52
  cursor: {
60
53
  providerId: cursor.providerId,
61
54
  providerName: cursor.providerName,
@@ -0,0 +1,150 @@
1
+ import { afterAll, expect, it } from 'vitest';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import http from 'node:http';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import express from 'express';
8
+ import { WebSocket } from 'ws';
9
+
10
+ import { attachRealtimeProxy, buildRealtimeProxyWsUrl } from './realtime-proxy.js';
11
+ import { createRequestSecurityRuntime } from './security/request-security.js';
12
+ import { createTerminalRuntime } from './terminal/runtime.js';
13
+ import { readTerminalWsControlFrame } from './terminal/terminal-ws-protocol.js';
14
+
15
+ const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
16
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-proxy-terminal-'));
17
+ process.env.OPENCHAMBER_DATA_DIR = dataDir;
18
+ const { createUiAuth } = await import('./ui-auth/ui-auth.js');
19
+
20
+ afterAll(() => {
21
+ if (previousDataDir === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
22
+ else process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
23
+ fs.rmSync(dataDir, { recursive: true, force: true });
24
+ });
25
+
26
+ const listen = async (server) => {
27
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
28
+ return `http://127.0.0.1:${server.address().port}`;
29
+ };
30
+
31
+ const login = async (origin, password) => {
32
+ const response = await fetch(`${origin}/auth/session`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ password }),
36
+ });
37
+ expect(response.status).toBe(200);
38
+ const cookie = response.headers.get('set-cookie').split(';')[0];
39
+ const minted = await fetch(`${origin}/auth/url-token`, {
40
+ method: 'POST',
41
+ headers: { Cookie: cookie, 'Content-Type': 'application/json' },
42
+ body: '{}',
43
+ });
44
+ expect(minted.status).toBe(200);
45
+ const { token } = await minted.json();
46
+ expect(token).toBeTruthy();
47
+ return token;
48
+ };
49
+
50
+ const connect = (url, origin = 'openchamber-ui://app') => new Promise((resolve, reject) => {
51
+ const socket = new WebSocket(url, { origin });
52
+ let hello = null;
53
+ let status = null;
54
+ const timeout = setTimeout(() => {
55
+ socket.terminate();
56
+ reject(new Error('Timed out waiting for terminal handshake'));
57
+ }, 3000);
58
+ socket.on('upgrade', (response) => { status = response.statusCode; });
59
+ socket.on('unexpected-response', (_request, response) => {
60
+ status = response.statusCode;
61
+ response.resume();
62
+ socket.terminate();
63
+ });
64
+ socket.on('message', (raw) => {
65
+ hello = readTerminalWsControlFrame(raw);
66
+ socket.close();
67
+ });
68
+ socket.on('error', () => {});
69
+ socket.on('close', (code) => {
70
+ clearTimeout(timeout);
71
+ resolve({ hello, status, code });
72
+ });
73
+ });
74
+
75
+ it('connects through the desktop proxy to an authenticated terminal without weakening either gate', async () => {
76
+ const password = crypto.randomBytes(24).toString('hex');
77
+ const remoteAuth = createUiAuth({ password });
78
+ const localAuth = createUiAuth({ password });
79
+ const remoteApp = express();
80
+ const localApp = express();
81
+ for (const [app, auth] of [[remoteApp, remoteAuth], [localApp, localAuth]]) {
82
+ app.use(express.json());
83
+ app.post('/auth/session', auth.handleSessionCreate);
84
+ app.post('/auth/url-token', auth.handleUrlAuthToken);
85
+ }
86
+ const remote = http.createServer(remoteApp);
87
+ const local = http.createServer(localApp);
88
+ const security = createRequestSecurityRuntime({ readSettingsFromDiskMigrated: async () => ({}) });
89
+ const received = [];
90
+ remote.on('upgrade', (req) => {
91
+ received.push({ origin: req.headers.origin, custom: req.headers['x-proxy-test'] });
92
+ });
93
+ const rejected = [];
94
+ const terminal = createTerminalRuntime({
95
+ app: remoteApp, server: remote, fs, path, uiAuthController: remoteAuth,
96
+ buildAugmentedPath: () => process.env.PATH || '',
97
+ searchPathFor: () => null,
98
+ isExecutable: () => false,
99
+ isRequestOriginAllowed: security.isRequestOriginAllowed,
100
+ rejectWebSocketUpgrade: (socket, status, reason) => {
101
+ rejected.push(status);
102
+ security.rejectWebSocketUpgrade(socket, status, reason);
103
+ },
104
+ TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS: 30_000,
105
+ });
106
+ let proxy;
107
+ try {
108
+ const remoteOrigin = await listen(remote);
109
+ const localOrigin = await listen(local);
110
+ proxy = attachRealtimeProxy({
111
+ app: localApp, server: local,
112
+ getDesktopRuntimeConfig: () => ({
113
+ apiBaseUrl: remoteOrigin,
114
+ requestHeaders: { 'X-Proxy-Test': 'present' },
115
+ }),
116
+ getUiAuthController: () => localAuth,
117
+ isRequestOriginAllowed: security.isRequestOriginAllowed,
118
+ });
119
+ const remoteToken = await login(remoteOrigin, password);
120
+ const localToken = await login(localOrigin, password);
121
+ const target = new URL('/api/terminal/ws', remoteOrigin);
122
+ target.protocol = 'ws:';
123
+ target.searchParams.set('oc_url_token', remoteToken);
124
+ const proxied = new URL(buildRealtimeProxyWsUrl(localOrigin, target.toString()));
125
+ proxied.searchParams.set('oc_url_token', localToken);
126
+
127
+ expect((await connect(target.toString())).hello).toEqual({ t: 'hello', v: 3 });
128
+ expect((await connect(proxied.toString())).hello).toEqual({ t: 'hello', v: 3 });
129
+ expect(received.at(-1)).toEqual({ origin: remoteOrigin, custom: 'present' });
130
+ expect(rejected).toEqual([]);
131
+
132
+ const beforeRejectedLocal = received.length;
133
+ expect((await connect(proxied.toString(), 'https://untrusted.example')).status).toBe(403);
134
+ const unauthenticatedLocal = new URL(proxied);
135
+ unauthenticatedLocal.searchParams.delete('oc_url_token');
136
+ expect((await connect(unauthenticatedLocal.toString())).status).toBe(401);
137
+ expect(received).toHaveLength(beforeRejectedLocal);
138
+
139
+ target.searchParams.delete('oc_url_token');
140
+ proxied.searchParams.set('url', target.toString());
141
+ expect((await connect(proxied.toString())).hello).toBeNull();
142
+ expect(rejected).toEqual([401]);
143
+ } finally {
144
+ proxy?.stop();
145
+ await terminal.shutdown();
146
+ remoteAuth.dispose();
147
+ localAuth.dispose();
148
+ await Promise.all([remote, local].map((server) => new Promise((resolve) => server.close(resolve))));
149
+ }
150
+ });
@@ -200,6 +200,8 @@ export const attachRealtimeProxy = ({ app, server, getDesktopRuntimeConfig, getU
200
200
 
201
201
  const upstream = new WebSocket(resolved.target.toString(), {
202
202
  headers: resolved.requestHeaders,
203
+ // ws does not send Origin by default; protected upstreams require it.
204
+ origin: `${resolved.target.protocol === 'wss:' ? 'https:' : 'http:'}//${resolved.target.host}`,
203
205
  });
204
206
  const pending = [];
205
207