@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,262 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { GUEST_FILE_CONTENT_MAX, GUEST_FILE_LIST_MAX } from '@openchamber/sdk';
7
+
8
+ import {
9
+ expandHome,
10
+ globToRegExp,
11
+ matchesFilesystemPattern,
12
+ resolveGuestFilePath,
13
+ runGuestFileOperation,
14
+ } from './files.js';
15
+
16
+ const tempDirs = [];
17
+
18
+ const makeTemp = async (prefix) => {
19
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
20
+ const real = await fs.realpath(dir);
21
+ tempDirs.push(real);
22
+ return real;
23
+ };
24
+
25
+ afterEach(async () => {
26
+ await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
27
+ });
28
+
29
+ describe('expandHome and globToRegExp', () => {
30
+ test('expands ~ and ~/ against the home directory', () => {
31
+ expect(expandHome('~/.config/x', '/home/ada')).toBe('/home/ada/.config/x');
32
+ expect(expandHome('~/.config/x', '/home/ada/')).toBe('/home/ada/.config/x');
33
+ expect(expandHome('~', '/home/ada')).toBe('/home/ada');
34
+ expect(expandHome('/etc/x', '/home/ada')).toBe('/etc/x');
35
+ });
36
+
37
+ test('** spans directories, * and ? stay inside one segment', () => {
38
+ expect(globToRegExp('/a/**/c.txt').test('/a/c.txt')).toBe(true);
39
+ expect(globToRegExp('/a/**/c.txt').test('/a/b/x/c.txt')).toBe(true);
40
+ expect(globToRegExp('/a/*.txt').test('/a/c.txt')).toBe(true);
41
+ expect(globToRegExp('/a/*.txt').test('/a/b/c.txt')).toBe(false);
42
+ expect(globToRegExp('/a/?.txt').test('/a/c.txt')).toBe(true);
43
+ expect(globToRegExp('/a/?.txt').test('/a/cc.txt')).toBe(false);
44
+ expect(globToRegExp('/a/b.txt').test('/a/bxtxt')).toBe(false);
45
+ });
46
+
47
+ test('a directory pattern matches the directory and everything below', () => {
48
+ const pattern = globToRegExp('/tmp/probe/**');
49
+ expect(pattern.test('/tmp/probe')).toBe(true);
50
+ expect(pattern.test('/tmp/probe/a')).toBe(true);
51
+ expect(pattern.test('/tmp/probe/a/b/c')).toBe(true);
52
+ expect(pattern.test('/tmp/probes')).toBe(false);
53
+ expect(pattern.test('/tmp')).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe('matchesFilesystemPattern', () => {
58
+ test('matches on the expanded home pattern only', () => {
59
+ const patterns = ['~/.config/opencode/opencode.json', '/tmp/probe/**'];
60
+ expect(matchesFilesystemPattern('/home/ada/.config/opencode/opencode.json', patterns, '/home/ada')).toBe(true);
61
+ expect(matchesFilesystemPattern('/home/ada/.config/opencode/other.json', patterns, '/home/ada')).toBe(false);
62
+ expect(matchesFilesystemPattern('/tmp/probe/deep/file', patterns, '/home/ada')).toBe(true);
63
+ expect(matchesFilesystemPattern('/etc/hosts', patterns, '/home/ada')).toBe(false);
64
+ expect(matchesFilesystemPattern('/etc/hosts', [], '/home/ada')).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('resolveGuestFilePath', () => {
69
+ test('accepts a relative path inside the project and refuses .. anywhere', async () => {
70
+ const project = await makeTemp('oc-files-project-');
71
+ await fs.writeFile(path.join(project, 'README.md'), '# hi');
72
+ const inside = await resolveGuestFilePath({ path: 'README.md', projectDirectory: project, patterns: [], homeDir: os.homedir() });
73
+ expect(inside).toEqual({ ok: true, scope: 'project', absolute: path.join(project, 'README.md') });
74
+ const dot = await resolveGuestFilePath({ path: '.', projectDirectory: project, patterns: [], homeDir: os.homedir() });
75
+ expect(dot).toEqual({ ok: true, scope: 'project', absolute: project });
76
+ for (const bad of ['../etc/passwd', 'src/../../x', '~/../x', '/tmp/../etc', 'a\\b', 'a\0b', '']) {
77
+ const result = await resolveGuestFilePath({ path: bad, projectDirectory: project, patterns: ['/**'], homeDir: os.homedir() });
78
+ expect(result).toMatchObject({ ok: false, code: 'BAD_PATH' });
79
+ }
80
+ });
81
+
82
+ test('is NO_DIRECTORY for a relative path without a project', async () => {
83
+ const result = await resolveGuestFilePath({ path: 'README.md', projectDirectory: null, patterns: [], homeDir: os.homedir() });
84
+ expect(result).toMatchObject({ ok: false, code: 'NO_DIRECTORY' });
85
+ });
86
+
87
+ test('refuses a symlink that escapes the project', async () => {
88
+ const project = await makeTemp('oc-files-project-');
89
+ const outside = await makeTemp('oc-files-outside-');
90
+ await fs.writeFile(path.join(outside, 'secret.txt'), 'nope');
91
+ await fs.symlink(outside, path.join(project, 'link'));
92
+ const viaLink = await resolveGuestFilePath({ path: 'link/secret.txt', projectDirectory: project, patterns: [], homeDir: os.homedir() });
93
+ expect(viaLink).toMatchObject({ ok: false, code: 'BAD_PATH' });
94
+ // A file that does not exist yet under the escaping link is still outside.
95
+ const newViaLink = await resolveGuestFilePath({ path: 'link/new.txt', projectDirectory: project, patterns: [], homeDir: os.homedir() });
96
+ expect(newViaLink).toMatchObject({ ok: false, code: 'BAD_PATH' });
97
+ });
98
+
99
+ test('accepts an outside path only when a declared pattern covers its canonical form', async () => {
100
+ const home = await makeTemp('oc-files-home-');
101
+ const probe = await makeTemp('oc-files-probe-');
102
+ await fs.mkdir(path.join(home, '.config', 'opencode'), { recursive: true });
103
+ const declared = await resolveGuestFilePath({
104
+ path: '~/.config/opencode/opencode.json',
105
+ projectDirectory: null,
106
+ patterns: ['~/.config/opencode/opencode.json', `${probe}/**`],
107
+ homeDir: home,
108
+ });
109
+ expect(declared).toEqual({ ok: true, scope: 'filesystem', absolute: path.join(home, '.config', 'opencode', 'opencode.json') });
110
+ const below = await resolveGuestFilePath({ path: `${probe}/a/b.txt`, projectDirectory: null, patterns: [`${probe}/**`], homeDir: home });
111
+ expect(below).toEqual({ ok: true, scope: 'filesystem', absolute: path.join(probe, 'a', 'b.txt') });
112
+ const undeclared = await resolveGuestFilePath({ path: '/etc/hosts', projectDirectory: null, patterns: ['~/.config/opencode/opencode.json'], homeDir: home });
113
+ expect(undeclared).toMatchObject({ ok: false, code: 'BAD_PATH' });
114
+ const none = await resolveGuestFilePath({ path: `${probe}/a`, projectDirectory: null, patterns: [], homeDir: home });
115
+ expect(none).toMatchObject({ ok: false, code: 'BAD_PATH' });
116
+ });
117
+
118
+ test('matches through a symlinked pattern prefix and refuses a symlink out of the granted tree', async () => {
119
+ const real = await makeTemp('oc-files-real-');
120
+ const holder = await makeTemp('oc-files-holder-');
121
+ const outside = await makeTemp('oc-files-outside-');
122
+ const link = path.join(holder, 'link');
123
+ await fs.symlink(real, link);
124
+ // Pattern names the symlink; the candidate canonicalizes to the real dir.
125
+ const viaLinkPattern = await resolveGuestFilePath({ path: `${link}/x.txt`, projectDirectory: null, patterns: [`${link}/**`], homeDir: holder });
126
+ expect(viaLinkPattern).toEqual({ ok: true, scope: 'filesystem', absolute: path.join(real, 'x.txt') });
127
+ // A symlink inside the granted tree pointing elsewhere is refused.
128
+ await fs.symlink(outside, path.join(real, 'escape'));
129
+ const escape = await resolveGuestFilePath({ path: `${real}/escape/y.txt`, projectDirectory: null, patterns: [`${real}/**`], homeDir: holder });
130
+ expect(escape).toMatchObject({ ok: false, code: 'BAD_PATH' });
131
+ });
132
+ });
133
+
134
+ describe('runGuestFileOperation', () => {
135
+ const base = async () => {
136
+ const project = await makeTemp('oc-files-project-');
137
+ const probe = await makeTemp('oc-files-probe-');
138
+ return {
139
+ project,
140
+ probe,
141
+ run: (overrides) => runGuestFileOperation({
142
+ projectDirectory: project,
143
+ patterns: [`${probe}/**`],
144
+ grants: ['files', 'filesystem'],
145
+ homeDir: os.homedir(),
146
+ ...overrides,
147
+ }),
148
+ };
149
+ };
150
+
151
+ test('needs files for the project and filesystem for outside', async () => {
152
+ const { probe, run } = await base();
153
+ expect(await run({ op: 'stat', path: 'README.md', grants: ['filesystem'] })).toMatchObject({ ok: false, code: 'NOT_GRANTED' });
154
+ expect(await run({ op: 'stat', path: `${probe}/x`, grants: ['files'] })).toMatchObject({ ok: false, code: 'NOT_GRANTED' });
155
+ expect(await run({ op: 'stat', path: 'README.md', grants: [] })).toMatchObject({ ok: false, code: 'NOT_GRANTED' });
156
+ expect(await run({ op: 'stat', path: 'README.md' })).toEqual({ ok: true, result: { kind: 'missing', size: 0, mtime: 0 } });
157
+ });
158
+
159
+ test('reads, stats, and reports a missing file as NOT_FOUND', async () => {
160
+ const { project, run } = await base();
161
+ await fs.writeFile(path.join(project, 'README.md'), '# hi');
162
+ expect(await run({ op: 'read', path: 'README.md' })).toEqual({ ok: true, result: { content: '# hi' } });
163
+ expect(await run({ op: 'read', path: 'missing.md' })).toMatchObject({ ok: false, code: 'NOT_FOUND' });
164
+ expect(await run({ op: 'read', path: '.' })).toMatchObject({ ok: false, code: 'BAD_PATH' });
165
+ const stat = await run({ op: 'stat', path: 'README.md' });
166
+ expect(stat).toMatchObject({ ok: true, result: { kind: 'file', size: 4 } });
167
+ expect(stat.result.mtime).toBeGreaterThan(0);
168
+ expect(await run({ op: 'stat', path: '.' })).toMatchObject({ ok: true, result: { kind: 'directory' } });
169
+ });
170
+
171
+ test('refuses to read a file over the content cap', async () => {
172
+ const { project, run } = await base();
173
+ const fsPromises = {
174
+ ...fs,
175
+ stat: async (target) => ({
176
+ ...(await fs.stat(project)),
177
+ isDirectory: () => false,
178
+ isFile: () => true,
179
+ size: GUEST_FILE_CONTENT_MAX + 1,
180
+ target,
181
+ }),
182
+ realpath: fs.realpath.bind(fs),
183
+ readFile: async () => {
184
+ throw new Error('should not read');
185
+ },
186
+ };
187
+ expect(await run({ op: 'read', path: 'big.txt', fsPromises })).toMatchObject({ ok: false, code: 'FILE_TOO_LARGE' });
188
+ });
189
+
190
+ test('writes atomically, creating parents, and refuses oversized content', async () => {
191
+ const { probe, run } = await base();
192
+ const target = path.join(probe, 'nested', 'deep', 'config.json');
193
+ const renames = [];
194
+ const fsPromises = {
195
+ ...fs,
196
+ realpath: fs.realpath.bind(fs),
197
+ rename: async (from, to) => {
198
+ // The target must not exist (or be complete) before the rename lands.
199
+ const before = await fs.readFile(to, 'utf8').catch(() => null);
200
+ renames.push({ from, to, before });
201
+ return fs.rename(from, to);
202
+ },
203
+ };
204
+ expect(await run({ op: 'write', path: target, content: '{"a":1}', fsPromises })).toEqual({ ok: true, result: { written: true } });
205
+ expect(await fs.readFile(target, 'utf8')).toBe('{"a":1}');
206
+ expect(renames).toHaveLength(1);
207
+ expect(renames[0].to).toBe(target);
208
+ expect(renames[0].from).not.toBe(target);
209
+ expect(renames[0].before).toBeNull();
210
+ // Overwrite: the previous content stays whole until the rename.
211
+ expect(await run({ op: 'write', path: target, content: '{"a":2}', fsPromises })).toEqual({ ok: true, result: { written: true } });
212
+ expect(renames[1].before).toBe('{"a":1}');
213
+ expect(await fs.readFile(target, 'utf8')).toBe('{"a":2}');
214
+ expect(await fs.readdir(path.dirname(target))).toEqual(['config.json']);
215
+ expect(await run({ op: 'write', path: target, content: 'x'.repeat(GUEST_FILE_CONTENT_MAX + 1) })).toMatchObject({ ok: false, code: 'FILE_TOO_LARGE' });
216
+ expect(await fs.readFile(target, 'utf8')).toBe('{"a":2}');
217
+ });
218
+
219
+ test('lists sorted entries with kinds and caps the list', async () => {
220
+ const { project, run } = await base();
221
+ await fs.mkdir(path.join(project, 'src'));
222
+ await fs.writeFile(path.join(project, 'b.txt'), '');
223
+ await fs.writeFile(path.join(project, 'a.txt'), '');
224
+ await fs.symlink(path.join(project, 'a.txt'), path.join(project, 'link'));
225
+ expect(await run({ op: 'list', path: '.' })).toEqual({
226
+ ok: true,
227
+ result: {
228
+ entries: [
229
+ { name: 'a.txt', kind: 'file' },
230
+ { name: 'b.txt', kind: 'file' },
231
+ { name: 'link', kind: 'other' },
232
+ { name: 'src', kind: 'directory' },
233
+ ],
234
+ },
235
+ });
236
+ expect(await run({ op: 'list', path: 'a.txt' })).toMatchObject({ ok: false, code: 'BAD_PATH' });
237
+ expect(await run({ op: 'list', path: 'nope' })).toMatchObject({ ok: false, code: 'NOT_FOUND' });
238
+ const many = await makeTemp('oc-files-many-');
239
+ await Promise.all(Array.from({ length: GUEST_FILE_LIST_MAX + 5 }, (_, index) => (
240
+ fs.writeFile(path.join(many, `f${String(index).padStart(5, '0')}`), '')
241
+ )));
242
+ const capped = await run({ op: 'list', path: many, patterns: [`${many}/**`] });
243
+ expect(capped.ok).toBe(true);
244
+ expect(capped.result.entries).toHaveLength(GUEST_FILE_LIST_MAX);
245
+ expect(capped.result.entries[0].name).toBe('f00000');
246
+ });
247
+
248
+ test('maps an OS permission error to DENIED', async () => {
249
+ const { project, run } = await base();
250
+ await fs.writeFile(path.join(project, 'README.md'), '# hi');
251
+ const fsPromises = {
252
+ ...fs,
253
+ realpath: fs.realpath.bind(fs),
254
+ readFile: async () => {
255
+ const error = new Error('EACCES');
256
+ error.code = 'EACCES';
257
+ throw error;
258
+ },
259
+ };
260
+ expect(await run({ op: 'read', path: 'README.md', fsPromises })).toMatchObject({ ok: false, code: 'DENIED' });
261
+ });
262
+ });
@@ -0,0 +1,81 @@
1
+ import { resolveIntegrationApi } from '@openchamber/sdk';
2
+
3
+ // What a grant covered when the user approved it. `filesystem` is the
4
+ // declared pattern list, `network` the API origin, `service` the exec names
5
+ // and socket ids the dialog showed. A newer package that changes any of
6
+ // these has not been approved for that capability, whatever the stored
7
+ // grant list says, so the card asks again and the proxies refuse meanwhile.
8
+
9
+ const sortedUnique = (values) => [...new Set(values)].sort();
10
+
11
+ /**
12
+ * @param {{ filesystem?: string[], integration?: object, service?: { permissions?: { exec?: string[], sockets?: Array<{ id: string }> } } }} guest
13
+ * @returns {{ filesystem?: string[], apiOrigin?: string, service?: { exec: string[], sockets: string[] } }}
14
+ */
15
+ export const guestGrantScope = (guest) => {
16
+ const scope = {};
17
+ if (Array.isArray(guest.filesystem) && guest.filesystem.length > 0) {
18
+ scope.filesystem = sortedUnique(guest.filesystem);
19
+ }
20
+ if (guest.integration) {
21
+ const api = resolveIntegrationApi(guest.integration);
22
+ if (api?.apiOrigin) {
23
+ scope.apiOrigin = api.apiOrigin;
24
+ }
25
+ // The stored refresh token and client secret travel to these two URLs;
26
+ // a package that moves either one has not been approved for them.
27
+ if (guest.integration.oauth) {
28
+ scope.oauth = {
29
+ authorizeUrl: guest.integration.oauth.authorizeUrl,
30
+ tokenUrl: guest.integration.oauth.tokenUrl,
31
+ };
32
+ }
33
+ }
34
+ if (guest.service) {
35
+ scope.service = {
36
+ exec: sortedUnique(guest.service.permissions?.exec ?? []),
37
+ sockets: sortedUnique((guest.service.permissions?.sockets ?? []).map((binding) => binding.id)),
38
+ };
39
+ }
40
+ return scope;
41
+ };
42
+
43
+ const sameOauth = (a, b) => (
44
+ (!a && !b) || Boolean(a && b && a.authorizeUrl === b.authorizeUrl && a.tokenUrl === b.tokenUrl)
45
+ );
46
+
47
+ /**
48
+ * Whether the credentials pasted or minted for one integration may still be
49
+ * used with the package as it is now: same API origin and same OAuth
50
+ * endpoints. Otherwise the tokens are dropped at re-approval.
51
+ */
52
+ export const sameCredentialTarget = (stored, current) => (
53
+ Boolean(stored?.apiOrigin) && stored.apiOrigin === current.apiOrigin && sameOauth(stored.oauth, current.oauth)
54
+ );
55
+
56
+ const sameList = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
57
+
58
+ /**
59
+ * The grants that still hold for the package as it is now. A scoped
60
+ * capability (`filesystem`, `network`, `service`) only counts when the stored
61
+ * scope equals the current one; without a stored scope it never counts.
62
+ * @param {string[]} granted
63
+ * @param {ReturnType<typeof guestGrantScope> | undefined} stored
64
+ * @param {ReturnType<typeof guestGrantScope>} current
65
+ */
66
+ export const effectiveGrants = (granted, stored, current) => granted.filter((capability) => {
67
+ if (capability === 'filesystem') {
68
+ return Boolean(stored?.filesystem) && sameList(stored.filesystem, current.filesystem ?? []);
69
+ }
70
+ if (capability === 'network') {
71
+ return Boolean(stored?.apiOrigin)
72
+ && stored.apiOrigin === current.apiOrigin
73
+ && sameOauth(stored.oauth, current.oauth);
74
+ }
75
+ if (capability === 'service') {
76
+ return Boolean(stored?.service)
77
+ && sameList(stored.service.exec, current.service?.exec ?? [])
78
+ && sameList(stored.service.sockets, current.service?.sockets ?? []);
79
+ }
80
+ return true;
81
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { effectiveGrants, guestGrantScope, sameCredentialTarget } from './grant-scope.js';
4
+
5
+ const guest = {
6
+ filesystem: ['~/notes/**', '~/.config/opencode/opencode.json'],
7
+ integration: {
8
+ name: 'Acme',
9
+ description: 'Tasks',
10
+ token: { apiOrigin: 'https://api.acme.example' },
11
+ },
12
+ service: {
13
+ entry: 'service/main.js',
14
+ runtime: 'host',
15
+ permissions: { exec: ['docker'], sockets: [{ id: 'docker', candidatesByPlatform: {} }] },
16
+ },
17
+ };
18
+
19
+ describe('guestGrantScope', () => {
20
+ test('captures sorted patterns, the API origin, and service permissions', () => {
21
+ expect(guestGrantScope(guest)).toEqual({
22
+ filesystem: ['~/.config/opencode/opencode.json', '~/notes/**'],
23
+ apiOrigin: 'https://api.acme.example',
24
+ service: { exec: ['docker'], sockets: ['docker'] },
25
+ });
26
+ expect(guestGrantScope({})).toEqual({});
27
+ expect(guestGrantScope({ integration: { name: 'L', description: 'x', host: { provider: 'linear' } } }))
28
+ .toEqual({ apiOrigin: 'https://api.linear.app' });
29
+ });
30
+ });
31
+
32
+ describe('effectiveGrants', () => {
33
+ const granted = ['prompt', 'filesystem', 'network', 'service'];
34
+
35
+ test('keeps every grant while the scope is what the user approved', () => {
36
+ const scope = guestGrantScope(guest);
37
+ expect(effectiveGrants(granted, scope, scope)).toEqual(granted);
38
+ });
39
+
40
+ test('drops a scoped grant when the package widened it', () => {
41
+ const approved = guestGrantScope(guest);
42
+ const wider = guestGrantScope({ ...guest, filesystem: ['~/**'] });
43
+ expect(effectiveGrants(granted, approved, wider)).toEqual(['prompt', 'network', 'service']);
44
+ const moved = guestGrantScope({ ...guest, integration: { ...guest.integration, token: { apiOrigin: 'https://evil.example' } } });
45
+ expect(effectiveGrants(granted, approved, moved)).toEqual(['prompt', 'filesystem', 'service']);
46
+ const moreExec = guestGrantScope({ ...guest, service: { ...guest.service, permissions: { exec: ['docker', 'kubectl'], sockets: guest.service.permissions.sockets } } });
47
+ expect(effectiveGrants(granted, approved, moreExec)).toEqual(['prompt', 'filesystem', 'network']);
48
+ });
49
+
50
+ test('never counts a scoped grant without a recorded scope', () => {
51
+ expect(effectiveGrants(granted, undefined, guestGrantScope(guest))).toEqual(['prompt']);
52
+ });
53
+ });
54
+
55
+ describe('oauth endpoints', () => {
56
+ const oauthGuest = {
57
+ integration: {
58
+ name: 'Acme',
59
+ description: 'Tasks',
60
+ oauth: { authorizeUrl: 'https://acme.example/authorize', tokenUrl: 'https://acme.example/token', apiOrigin: 'https://api.acme.example' },
61
+ },
62
+ };
63
+
64
+ test('a moved token endpoint drops the network grant and the credentials', () => {
65
+ const approved = guestGrantScope(oauthGuest);
66
+ expect(approved).toEqual({
67
+ apiOrigin: 'https://api.acme.example',
68
+ oauth: { authorizeUrl: 'https://acme.example/authorize', tokenUrl: 'https://acme.example/token' },
69
+ });
70
+ const moved = guestGrantScope({ integration: { ...oauthGuest.integration, oauth: { ...oauthGuest.integration.oauth, tokenUrl: 'https://evil.example/token' } } });
71
+ expect(effectiveGrants(['network'], approved, moved)).toEqual([]);
72
+ expect(sameCredentialTarget(approved, moved)).toBe(false);
73
+ expect(sameCredentialTarget(approved, guestGrantScope(oauthGuest))).toBe(true);
74
+ });
75
+ });
@@ -0,0 +1,73 @@
1
+ import { resolveIntegrationAuth } from '@openchamber/sdk';
2
+
3
+ import { readTrimmedString } from '../linear/parse.js';
4
+ import { GuestOAuthError, toPublicGuestAuth } from './oauth.js';
5
+
6
+ const isHostLinear = (integration) => (
7
+ resolveIntegrationAuth(integration ?? {}) === 'host'
8
+ && integration?.host?.provider === 'linear'
9
+ );
10
+
11
+ const linearAccountLabel = (auth) => (
12
+ readTrimmedString(auth?.user?.displayName)
13
+ || readTrimmedString(auth?.user?.name)
14
+ || readTrimmedString(auth?.organization?.name)
15
+ );
16
+
17
+ export const toGuestAuthResponse = async (integration, stored) => {
18
+ const published = toPublicGuestAuth(stored, integration);
19
+ if (!isHostLinear(integration)) {
20
+ return published;
21
+ }
22
+ const { getLinearAuth } = await import('../linear/index.js');
23
+ const auth = getLinearAuth();
24
+ return {
25
+ ...published,
26
+ connected: Boolean(auth?.accessToken),
27
+ account: linearAccountLabel(auth),
28
+ hasClient: true,
29
+ };
30
+ };
31
+
32
+ export const startHostGuestAuthorization = async (integration) => {
33
+ if (!isHostLinear(integration)) {
34
+ return null;
35
+ }
36
+ try {
37
+ const { startAuthorization } = await import('../linear/index.js');
38
+ return await startAuthorization({ origin: 'web' });
39
+ } catch (error) {
40
+ if (error instanceof GuestOAuthError) {
41
+ throw error;
42
+ }
43
+ const message = error instanceof Error ? error.message : 'Could not start Linear.';
44
+ throw new GuestOAuthError(message, 'HOST_AUTH_FAILED');
45
+ }
46
+ };
47
+
48
+ export const disconnectHostGuest = async (integration) => {
49
+ if (!isHostLinear(integration)) {
50
+ return false;
51
+ }
52
+ const { getLinearAuth, clearLinearAuth, revokeToken } = await import('../linear/index.js');
53
+ const auth = getLinearAuth();
54
+ if (auth?.refreshToken) {
55
+ await revokeToken(auth.refreshToken, 'refresh_token').catch(() => undefined);
56
+ } else if (auth?.accessToken) {
57
+ await revokeToken(auth.accessToken, 'access_token').catch(() => undefined);
58
+ }
59
+ clearLinearAuth(auth?.workspaceId);
60
+ return true;
61
+ };
62
+
63
+ export const resolveHostAccessToken = async (integration) => {
64
+ if (!isHostLinear(integration)) {
65
+ return null;
66
+ }
67
+ try {
68
+ const { getValidLinearAccessToken } = await import('../linear/index.js');
69
+ return await getValidLinearAccessToken();
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { toGuestAuthResponse } from './host-session.js';
4
+
5
+ describe('toGuestAuthResponse', () => {
6
+ test('leaves an oauth guest on the stored public slice', async () => {
7
+ const published = await toGuestAuthResponse({
8
+ name: 'GitLab',
9
+ description: 'Issues',
10
+ oauth: {
11
+ authorizeUrl: 'https://gitlab.com/oauth/authorize',
12
+ tokenUrl: 'https://gitlab.com/oauth/token',
13
+ apiOrigin: 'https://gitlab.com',
14
+ },
15
+ }, {
16
+ accessToken: 'tok',
17
+ account: 'ada',
18
+ clientId: 'app-1',
19
+ // Entered for these endpoints, so the card still counts it as a client.
20
+ clientTarget: { apiOrigin: 'https://gitlab.com', authorizeUrl: 'https://gitlab.com/oauth/authorize', tokenUrl: 'https://gitlab.com/oauth/token' },
21
+ settings: { 'project-path': 'group/project' },
22
+ });
23
+ expect(published).toEqual({
24
+ connected: true,
25
+ account: 'ada',
26
+ hasClient: true,
27
+ settings: { 'project-path': 'group/project' },
28
+ });
29
+ });
30
+ });
@@ -0,0 +1,6 @@
1
+ import { GUEST_SCROLLBAR_CSS } from '@openchamber/sdk';
2
+
3
+ /** Append instead of matching tags inside untrusted comments, scripts, or templates.
4
+ * Browsers accept the style after the document; its doctype and authored CSP stay intact.
5
+ */
6
+ export const injectGuestDocumentStyles = (html) => `${html}\n<style data-openchamber-guest-styles>${GUEST_SCROLLBAR_CSS}</style>`;
@@ -0,0 +1,45 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import express from 'express';
6
+ import request from 'supertest';
7
+ import { GUEST_SCROLLBAR_CSS } from '@openchamber/sdk';
8
+ import { injectGuestDocumentStyles } from './html-styles.js';
9
+ import { registerGuestRoutes } from './routes.js';
10
+ import { writeExtensionStore } from './persist.js';
11
+
12
+ const roots = [];
13
+ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); });
14
+
15
+ describe('guest document styles', () => {
16
+ test('preserves doctypes, CSP, and tag-like text inside authored scripts', () => {
17
+ const html = '<!doctype html><html><head><meta http-equiv="Content-Security-Policy" content="style-src \'self\'"><script>const text = "</head><head>";</script></head><body>Guest</body></html>';
18
+ const decorated = injectGuestDocumentStyles(html);
19
+ expect(decorated.startsWith(html)).toBe(true);
20
+ expect(decorated).toContain(GUEST_SCROLLBAR_CSS);
21
+ expect(decorated).toContain('data-openchamber-guest-styles');
22
+ });
23
+
24
+ test('serves scrollbar defaults to existing guests with or without an asset token', async () => {
25
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-styles-')); roots.push(root);
26
+ const packageRoot = path.join(root, 'guest'); await fs.mkdir(packageRoot);
27
+ const html = '<!doctype html><html><head></head><body><script src="main.js"></script></body></html>';
28
+ const script = 'console.log("existing bundle");';
29
+ await fs.writeFile(path.join(packageRoot, 'index.html'), html);
30
+ await fs.writeFile(path.join(packageRoot, 'main.js'), script);
31
+ await fs.writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: 'guest', version: '1.0.0', openchamber: { apiVersion: 1, contributes: { panel: { id: 'old-guest', name: 'Old guest', icon: 'apps', entry: 'index.html' } } } }));
32
+ await writeExtensionStore(path.join(root, 'extensions.json'), { paths: [packageRoot], sources: { [packageRoot]: 'path' } });
33
+ const app = express(); registerGuestRoutes(app, { openchamberDataDir: root });
34
+ for (const suffix of ['', '?oc_url_token=fixture-scope']) {
35
+ const response = await request(app).get(`/api/guests/old-guest/index.html${suffix}`).expect(200);
36
+ expect(response.headers['content-security-policy']).toBe('sandbox allow-scripts');
37
+ expect(response.headers['cache-control']).toBe('no-store');
38
+ expect(response.text).toContain(GUEST_SCROLLBAR_CSS);
39
+ expect(response.text.startsWith('<!doctype html>')).toBe(true);
40
+ expect(response.text).toContain(suffix ? 'main.js?oc_url_token=fixture-scope' : 'src="main.js"');
41
+ }
42
+ expect((await request(app).get('/api/guests/old-guest/main.js').expect(200)).text).toBe(script);
43
+ expect(await fs.readFile(path.join(packageRoot, 'index.html'), 'utf8')).toBe(html);
44
+ });
45
+ });
@@ -0,0 +1,73 @@
1
+ const RELATIVE_ASSET = /(<(?:script|link|img)\b[^>]*?\s(?:src|href)=)(["'])(?!https?:|\/\/|data:|#)([^"']+)\2/gi;
2
+
3
+ const withToken = (url, token) => {
4
+ if (url.includes('oc_url_token=')) {
5
+ return url;
6
+ }
7
+ const joiner = url.includes('?') ? '&' : '?';
8
+ return `${url}${joiner}oc_url_token=${encodeURIComponent(token)}`;
9
+ };
10
+
11
+ const firstQueryValue = (value) => (Array.isArray(value) ? value[0] : value);
12
+
13
+ /** Query `oc_url_token` for guest HTML. Empty when the query is missing or not a token. */
14
+ export const parseGuestUrlToken = (value) => {
15
+ const first = firstQueryValue(value);
16
+ if (first == null || first === '') {
17
+ return '';
18
+ }
19
+ return `${first}`;
20
+ };
21
+
22
+ const SCRIPT_SRC = /<script\b[^>]*?\ssrc=(["'])(?!https?:|\/\/|data:|#)([^"']+)\1/gi;
23
+
24
+ /** Relative `script src` values. Absolute, protocol-relative, data, and hash URLs are skipped. */
25
+ export const listRelativeGuestScriptHrefs = (html) => {
26
+ if (html === '') {
27
+ return [];
28
+ }
29
+ const hrefs = [];
30
+ SCRIPT_SRC.lastIndex = 0;
31
+ let match = SCRIPT_SRC.exec(html);
32
+ while (match) {
33
+ hrefs.push(match[2]);
34
+ match = SCRIPT_SRC.exec(html);
35
+ }
36
+ return hrefs;
37
+ };
38
+
39
+ /** Resolve a relative href against the entry HTML path. Stays inside the package. */
40
+ export const resolveGuestHtmlRelativePath = (entry, href) => {
41
+ const withoutHash = href.split('#')[0] ?? '';
42
+ const raw = withoutHash.split('?')[0];
43
+ if (raw === '' || raw.includes('\0') || raw.includes('\\')) {
44
+ return null;
45
+ }
46
+ const entryDir = entry.includes('/') ? entry.slice(0, entry.lastIndexOf('/')) : '';
47
+ const combined = entryDir === '' ? raw : `${entryDir}/${raw}`;
48
+ const segments = [];
49
+ for (const part of combined.split('/')) {
50
+ if (part === '' || part === '.') {
51
+ continue;
52
+ }
53
+ if (part === '..') {
54
+ if (segments.length === 0) {
55
+ return null;
56
+ }
57
+ segments.pop();
58
+ continue;
59
+ }
60
+ segments.push(part);
61
+ }
62
+ return segments.join('/');
63
+ };
64
+
65
+ /** Copy the iframe URL token onto relative script/link/img URLs in guest HTML. */
66
+ export const injectGuestAssetTokens = (html, token) => {
67
+ if (!token) {
68
+ return html;
69
+ }
70
+ return html.replace(RELATIVE_ASSET, (_match, prefix, quote, url) => {
71
+ return `${prefix}${quote}${withToken(url, token)}${quote}`;
72
+ });
73
+ };