@vantran-se/openchamber-web 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -4,52 +4,43 @@ import crypto from 'node:crypto';
4
4
  import { createRelayIdentityRuntime } from './identity.js';
5
5
  import { canonicalPublicJwkString } from './signing-key.js';
6
6
 
7
- // In-memory settings store standing in for the on-disk settings file.
8
- const makeSettingsStore = (initial = {}) => {
9
- let settings = { ...initial };
7
+ // In-memory key store standing in for the on-disk relay identity file.
8
+ const makeKeyStore = (initial = {}) => {
9
+ const slots = { ...initial };
10
10
  return {
11
- readSettingsFromDiskMigrated: async () => ({ ...settings }),
12
- writeSettingsToDisk: async (next) => {
13
- settings = { ...next };
14
- },
15
- peek: () => settings,
11
+ getOrCreate: async (slot, generate) => (slots[slot] ??= await generate()),
12
+ peek: () => slots,
16
13
  };
17
14
  };
18
15
 
19
16
  describe('relay identity', () => {
20
17
  it('derives a stable serverId from the signing key and persists both keypairs', async () => {
21
- const store = makeSettingsStore();
22
- const runtime = createRelayIdentityRuntime({ crypto, ...store });
18
+ const relayKeyStore = makeKeyStore();
19
+ const runtime = createRelayIdentityRuntime({ crypto, relayKeyStore });
23
20
  const identity = await runtime.getRelayIdentity();
24
21
 
25
- const stored = store.peek();
26
- expect(stored.relaySigningKey).toBeDefined();
27
- expect(stored.relayEncryptionKey).toBeDefined();
22
+ const stored = relayKeyStore.peek();
23
+ expect(stored.signing).toBeDefined();
24
+ expect(stored.encryption).toBeDefined();
28
25
 
29
26
  const expectedServerId = crypto
30
27
  .createHash('sha256')
31
- .update(canonicalPublicJwkString(stored.relaySigningKey.publicJwk))
28
+ .update(canonicalPublicJwkString(stored.signing.publicJwk))
32
29
  .digest('base64url');
33
30
  expect(identity.serverId).toBe(expectedServerId);
34
31
  expect(identity.hostEncPubJwk.crv).toBe('P-256');
35
32
  });
36
33
 
37
34
  it('reuses an existing signing key (serverId stays stable across installs)', async () => {
38
- const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
39
- void privateKey;
40
- const publicJwk = publicKey.export({ format: 'jwk' });
41
- const store = makeSettingsStore({
42
- relaySigningKey: {
43
- privateJwk: crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ format: 'jwk' }),
44
- publicJwk,
35
+ const pair = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
36
+ const relayKeyStore = makeKeyStore({
37
+ signing: {
38
+ privateJwk: pair.privateKey.export({ format: 'jwk' }),
39
+ publicJwk: pair.publicKey.export({ format: 'jwk' }),
45
40
  },
46
41
  });
47
- // Match private to public so importing works.
48
- const pair = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
49
- store.peek().relaySigningKey.privateJwk = pair.privateKey.export({ format: 'jwk' });
50
- store.peek().relaySigningKey.publicJwk = pair.publicKey.export({ format: 'jwk' });
51
42
 
52
- const runtime = createRelayIdentityRuntime({ crypto, ...store });
43
+ const runtime = createRelayIdentityRuntime({ crypto, relayKeyStore });
53
44
  const identity = await runtime.getRelayIdentity();
54
45
  const expected = crypto
55
46
  .createHash('sha256')
@@ -59,8 +50,7 @@ describe('relay identity', () => {
59
50
  });
60
51
 
61
52
  it('produces a verifiable relay auth signature', async () => {
62
- const store = makeSettingsStore();
63
- const runtime = createRelayIdentityRuntime({ crypto, ...store });
53
+ const runtime = createRelayIdentityRuntime({ crypto, relayKeyStore: makeKeyStore() });
64
54
  const identity = await runtime.getRelayIdentity();
65
55
  const { ts, sig, pk } = identity.signRelayAuth('host-control', null);
66
56
 
@@ -0,0 +1,141 @@
1
+ // Host relay identity keys (the signing and encryption keypairs, private JWKs
2
+ // included) live in their own 0600 file, `<data-dir>/relay-identity.json`,
3
+ // not in settings.json, so the private keys do not travel with every copy of
4
+ // the settings file.
5
+ //
6
+ // Installs that predate the split keep the keys in settings.json as
7
+ // `relaySigningKey` / `relayEncryptionKey`. The first load moves them here
8
+ // unchanged, so serverId and the E2EE trust anchor stay the same, then strips
9
+ // them from settings.json. An older build sharing the data dir no longer finds
10
+ // them there and mints its own identity, which it warns about loudly.
11
+ //
12
+ // A key is minted only when neither file holds it. Both files are read
13
+ // strictly: a corrupt or unreadable file throws instead of reading as "first
14
+ // run", because a new key orphans every paired device and push binding.
15
+
16
+ import { z } from 'zod';
17
+
18
+ const SLOTS = Object.freeze({
19
+ signing: { field: 'signingKey', legacySettingsKey: 'relaySigningKey' },
20
+ encryption: { field: 'encryptionKey', legacySettingsKey: 'relayEncryptionKey' },
21
+ });
22
+
23
+ const jwkSchema = z.looseObject({ kty: z.string().min(1) });
24
+ const jwkPairSchema = z.looseObject({ privateJwk: jwkSchema, publicJwk: jwkSchema });
25
+ // Unknown fields are kept so a newer build's additions survive our rewrites.
26
+ const storeSchema = z.looseObject({
27
+ signingKey: jwkPairSchema.optional(),
28
+ encryptionKey: jwkPairSchema.optional(),
29
+ });
30
+
31
+ /** @typedef {z.infer<typeof jwkPairSchema>} RelayJwkPair */
32
+ /** @typedef {'signing' | 'encryption'} RelayKeySlot */
33
+
34
+ const isJwkPair = (value) => jwkPairSchema.safeParse(value).success;
35
+
36
+ /**
37
+ * @param {{
38
+ * fsPromises: typeof import('node:fs/promises'),
39
+ * path: typeof import('node:path'),
40
+ * dataDir: string,
41
+ * readSettingsFromDiskMigrated: () => Promise<object>,
42
+ * readSettingsStrict: () => Promise<object>,
43
+ * writeSettingsToDisk: (settings: object) => Promise<void>,
44
+ * logger?: Pick<Console, 'warn'>,
45
+ * }} deps
46
+ */
47
+ export const createRelayKeyStore = ({
48
+ fsPromises,
49
+ path,
50
+ dataDir,
51
+ readSettingsFromDiskMigrated,
52
+ readSettingsStrict,
53
+ writeSettingsToDisk,
54
+ logger = console,
55
+ }) => {
56
+ const filePath = path.join(dataDir, 'relay-identity.json');
57
+ // The push relay and the private relay ask for the signing key at the same
58
+ // boot; serialized, the second caller reads what the first one wrote
59
+ // instead of minting a second key over it.
60
+ let queue = Promise.resolve();
61
+ const serialized = (task) => {
62
+ const run = queue.then(task, task);
63
+ queue = run.catch(() => {});
64
+ return run;
65
+ };
66
+
67
+ const readStore = async () => {
68
+ let raw;
69
+ try {
70
+ raw = await fsPromises.readFile(filePath, 'utf8');
71
+ } catch (error) {
72
+ if (error?.code === 'ENOENT') return {};
73
+ throw error;
74
+ }
75
+ const corrupt = (cause) => new Error(`Relay identity file is corrupt: ${filePath} (fix or remove it, then retry)`, { cause });
76
+ let json;
77
+ try {
78
+ json = JSON.parse(raw);
79
+ } catch (cause) {
80
+ throw corrupt(cause);
81
+ }
82
+ const parsed = storeSchema.safeParse(json);
83
+ if (!parsed.success) throw corrupt(parsed.error);
84
+ return parsed.data;
85
+ };
86
+
87
+ const writeStore = async (store) => {
88
+ const directory = path.dirname(filePath);
89
+ const created = await fsPromises.mkdir(directory, { recursive: true, mode: 0o700 });
90
+ if (created && process.platform !== 'win32') await fsPromises.chmod(directory, 0o700);
91
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
92
+ try {
93
+ await fsPromises.writeFile(tmp, JSON.stringify(store, null, 2), { encoding: 'utf8', mode: 0o600 });
94
+ if (process.platform !== 'win32') await fsPromises.chmod(tmp, 0o600);
95
+ await fsPromises.rename(tmp, filePath);
96
+ } catch (error) {
97
+ await fsPromises.rm(tmp, { force: true }).catch(() => {});
98
+ throw error;
99
+ }
100
+ };
101
+
102
+ // Drops from settings.json every legacy key this file already holds. Best
103
+ // effort: the identity is valid without it, and the next load retries.
104
+ const scrubLegacySettings = async (store) => {
105
+ try {
106
+ const raw = await readSettingsStrict();
107
+ const stale = Object.values(SLOTS)
108
+ .filter(({ field, legacySettingsKey }) => isJwkPair(store[field]) && Object.hasOwn(raw, legacySettingsKey))
109
+ .map(({ legacySettingsKey }) => legacySettingsKey);
110
+ if (stale.length === 0) return;
111
+ const next = { ...(await readSettingsFromDiskMigrated()), ...raw };
112
+ for (const key of stale) delete next[key];
113
+ await writeSettingsToDisk(next);
114
+ } catch (error) {
115
+ logger.warn('[relay-identity] Could not remove the moved relay keys from settings.json:', error);
116
+ }
117
+ };
118
+
119
+ /**
120
+ * @param {RelayKeySlot} slot
121
+ * @param {() => Promise<RelayJwkPair>} generate Mints a new keypair; called only when no file holds one.
122
+ * @returns {Promise<RelayJwkPair>}
123
+ */
124
+ const getOrCreate = (slot, generate) => serialized(async () => {
125
+ const { field, legacySettingsKey } = SLOTS[slot];
126
+ let store = await readStore();
127
+ if (!isJwkPair(store[field])) {
128
+ const legacy = (await readSettingsStrict())[legacySettingsKey];
129
+ // Another process may have moved or minted the key between the two reads.
130
+ store = await readStore();
131
+ if (!isJwkPair(store[field])) {
132
+ store = { ...store, [field]: isJwkPair(legacy) ? legacy : await generate() };
133
+ await writeStore(store);
134
+ }
135
+ }
136
+ await scrubLegacySettings(store);
137
+ return store[field];
138
+ });
139
+
140
+ return { getOrCreate };
141
+ };
@@ -0,0 +1,138 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import crypto from 'node:crypto';
6
+
7
+ import { createRelayKeyStore } from './key-store.js';
8
+
9
+ const makePair = () => {
10
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
11
+ return { privateJwk: privateKey.export({ format: 'jwk' }), publicJwk: publicKey.export({ format: 'jwk' }) };
12
+ };
13
+
14
+ let dataDir;
15
+ beforeEach(() => {
16
+ dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-relay-keys-'));
17
+ });
18
+ afterEach(() => {
19
+ fs.rmSync(dataDir, { recursive: true, force: true });
20
+ });
21
+
22
+ const identityPath = () => path.join(dataDir, 'relay-identity.json');
23
+ const settingsPath = () => path.join(dataDir, 'settings.json');
24
+ const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
25
+ const writeJson = (filePath, value) => fs.writeFileSync(filePath, JSON.stringify(value));
26
+
27
+ // Settings accessors over a real settings.json, strict like the app's.
28
+ const makeStore = ({ writeSettingsToDisk } = {}) => {
29
+ const readSettingsStrict = async () => {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(settingsPath(), 'utf8'));
32
+ } catch (error) {
33
+ if (error.code === 'ENOENT') return {};
34
+ throw error;
35
+ }
36
+ };
37
+ return createRelayKeyStore({
38
+ fsPromises: fs.promises,
39
+ path,
40
+ dataDir,
41
+ readSettingsFromDiskMigrated: async () => readSettingsStrict().catch(() => ({})),
42
+ readSettingsStrict,
43
+ writeSettingsToDisk: writeSettingsToDisk ?? (async (next) => writeJson(settingsPath(), next)),
44
+ logger: { warn: () => {} },
45
+ });
46
+ };
47
+
48
+ describe('relay key store', () => {
49
+ it('mints a key on first run into its own owner-only file, not settings.json', async () => {
50
+ writeJson(settingsPath(), { theme: 'dark' });
51
+ const pair = makePair();
52
+ let generations = 0;
53
+ const stored = await makeStore().getOrCreate('signing', async () => {
54
+ generations += 1;
55
+ return pair;
56
+ });
57
+
58
+ expect(stored).toEqual(pair);
59
+ expect(generations).toBe(1);
60
+ expect(readJson(identityPath()).signingKey).toEqual(pair);
61
+ expect(readJson(settingsPath())).toEqual({ theme: 'dark' });
62
+ if (process.platform !== 'win32') {
63
+ expect(fs.statSync(identityPath()).mode & 0o777).toBe(0o600);
64
+ }
65
+ });
66
+
67
+ it('moves legacy keys out of settings.json unchanged and keeps the other settings', async () => {
68
+ const signing = makePair();
69
+ const encryption = makePair();
70
+ writeJson(settingsPath(), { theme: 'dark', relaySigningKey: signing, relayEncryptionKey: encryption });
71
+ const store = makeStore();
72
+ const mint = async () => {
73
+ throw new Error('must not mint a key that settings.json already holds');
74
+ };
75
+
76
+ expect(await store.getOrCreate('signing', mint)).toEqual(signing);
77
+ expect(await store.getOrCreate('encryption', mint)).toEqual(encryption);
78
+
79
+ expect(readJson(identityPath())).toEqual({ signingKey: signing, encryptionKey: encryption });
80
+ expect(readJson(settingsPath())).toEqual({ theme: 'dark' });
81
+ });
82
+
83
+ it('strips from settings.json only the keys the identity file already holds', async () => {
84
+ writeJson(settingsPath(), { relaySigningKey: makePair(), relayEncryptionKey: makePair() });
85
+ await makeStore().getOrCreate('signing', async () => makePair());
86
+
87
+ const settings = readJson(settingsPath());
88
+ expect(settings.relaySigningKey).toBeUndefined();
89
+ expect(settings.relayEncryptionKey).toBeDefined();
90
+ });
91
+
92
+ it('keeps the identity file authoritative over a key an older build put back into settings.json', async () => {
93
+ const current = makePair();
94
+ writeJson(identityPath(), { signingKey: current });
95
+ writeJson(settingsPath(), { relaySigningKey: makePair() });
96
+
97
+ expect(await makeStore().getOrCreate('signing', async () => makePair())).toEqual(current);
98
+ expect(readJson(settingsPath()).relaySigningKey).toBeUndefined();
99
+ });
100
+
101
+ it('mints one key when two callers ask at the same time', async () => {
102
+ const store = makeStore();
103
+ let generations = 0;
104
+ const mint = async () => {
105
+ generations += 1;
106
+ return makePair();
107
+ };
108
+ const [first, second] = await Promise.all([store.getOrCreate('signing', mint), store.getOrCreate('signing', mint)]);
109
+
110
+ expect(generations).toBe(1);
111
+ expect(second).toEqual(first);
112
+ });
113
+
114
+ it('refuses to mint over a corrupt identity file', async () => {
115
+ fs.writeFileSync(identityPath(), '{"signingKey": {"unfinished');
116
+ await expect(makeStore().getOrCreate('signing', async () => makePair())).rejects.toThrow(/corrupt/);
117
+ expect(fs.readFileSync(identityPath(), 'utf8')).toBe('{"signingKey": {"unfinished');
118
+ });
119
+
120
+ it('refuses to mint while a corrupt settings.json may still hold the legacy key', async () => {
121
+ fs.writeFileSync(settingsPath(), '{"relaySigningKey": {"unfinished');
122
+ await expect(makeStore().getOrCreate('signing', async () => makePair())).rejects.toThrow();
123
+ expect(fs.existsSync(identityPath())).toBe(false);
124
+ });
125
+
126
+ it('still returns the key when stripping settings.json fails', async () => {
127
+ const signing = makePair();
128
+ writeJson(settingsPath(), { relaySigningKey: signing });
129
+ const store = makeStore({
130
+ writeSettingsToDisk: async () => {
131
+ throw new Error('disk full');
132
+ },
133
+ });
134
+
135
+ expect(await store.getOrCreate('signing', async () => makePair())).toEqual(signing);
136
+ expect(readJson(identityPath()).signingKey).toEqual(signing);
137
+ });
138
+ });
@@ -14,7 +14,7 @@
14
14
  import express from 'express';
15
15
 
16
16
  import { createRelayIdentityRuntime } from './identity.js';
17
- import { startRelayHost } from './host-client.js';
17
+ import { describeRelayError, startRelayHost } from './host-client.js';
18
18
  import { isEnterpriseMode, readEnterprisePolicy } from '../enterprise-mode.js';
19
19
 
20
20
  export const DEFAULT_RELAY_URL = 'wss://relay.openchamber.dev/ws';
@@ -59,6 +59,7 @@ export const relayBlockedByEnterprise = () => isEnterpriseMode() && pinnedRelayU
59
59
  * crypto: typeof import('node:crypto'),
60
60
  * readSettingsFromDiskMigrated: () => Promise<object>,
61
61
  * writeSettingsToDisk: (settings: object) => Promise<void>,
62
+ * relayKeyStore: ReturnType<typeof import('./key-store.js').createRelayKeyStore>,
62
63
  * getLocalPort: () => number,
63
64
  * logger?: Pick<Console, 'warn'>,
64
65
  * }} deps
@@ -67,9 +68,8 @@ export const createRelayService = ({
67
68
  crypto,
68
69
  readSettingsFromDiskMigrated,
69
70
  writeSettingsToDisk,
70
- // Strict settings reader (throws on corrupt/unreadable) gating identity
71
- // regeneration — see identity.js/signing-key.js.
72
- readSettingsStrict,
71
+ // Host relay identity keys, shared with the push relay (key-store.js).
72
+ relayKeyStore,
73
73
  getLocalPort,
74
74
  // Returns true when any paired device or pending pairing session uses the
75
75
  // relay transport. The relay lifecycle is driven purely by this demand.
@@ -86,7 +86,7 @@ export const createRelayService = ({
86
86
  allowPassiveHost = true,
87
87
  logger = console,
88
88
  }) => {
89
- const identityRuntime = createRelayIdentityRuntime({ crypto, readSettingsFromDiskMigrated, writeSettingsToDisk, readSettingsStrict });
89
+ const identityRuntime = createRelayIdentityRuntime({ crypto, relayKeyStore });
90
90
 
91
91
  let hostClient = null;
92
92
  let status = { state: 'disabled', lastError: null, connectedClients: 0 };
@@ -170,7 +170,7 @@ export const createRelayService = ({
170
170
  await start(relayUrl);
171
171
  }
172
172
  } catch (error) {
173
- logger.warn(`[Relay] claim watch failed: ${error?.message ?? error}`);
173
+ logger.warn(`[Relay] claim watch failed: ${describeRelayError(error)}`);
174
174
  }
175
175
  })();
176
176
  }, CLAIM_WATCH_INTERVAL_MS);
@@ -235,7 +235,7 @@ export const createRelayService = ({
235
235
  await start(config.relayUrl);
236
236
  }
237
237
  } catch (error) {
238
- logger.warn(`[Relay] startup failed: ${error?.message ?? error}`);
238
+ logger.warn(`[Relay] startup failed: ${describeRelayError(error)}`);
239
239
  }
240
240
  };
241
241
 
@@ -257,7 +257,7 @@ export const createRelayService = ({
257
257
  stop();
258
258
  }
259
259
  } catch (error) {
260
- logger.warn(`[Relay] reconcile failed: ${error?.message ?? error}`);
260
+ logger.warn(`[Relay] reconcile failed: ${describeRelayError(error)}`);
261
261
  }
262
262
  };
263
263
 
@@ -4,14 +4,19 @@ import crypto from 'node:crypto';
4
4
  import { createRelayService } from './service.js';
5
5
 
6
6
  const makeService = (options = {}) => {
7
- // In-memory settings store with a pre-seeded relay identity so the service
8
- // never regenerates a signing key during the test.
7
+ // In-memory key store with a pre-seeded signing key so the service never
8
+ // regenerates the serverId during the test.
9
9
  const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
10
- let settings = {
11
- relaySigningKey: {
10
+ const keySlots = {
11
+ signing: {
12
12
  privateJwk: privateKey.export({ format: 'jwk' }),
13
13
  publicJwk: publicKey.export({ format: 'jwk' }),
14
14
  },
15
+ };
16
+ const relayKeyStore = {
17
+ getOrCreate: async (slot, generate) => (keySlots[slot] ??= await generate()),
18
+ };
19
+ let settings = {
15
20
  privateRelay: { enabled: true, relayUrl: 'wss://relay.example.test/ws' },
16
21
  ...options.settings,
17
22
  };
@@ -26,7 +31,7 @@ const makeService = (options = {}) => {
26
31
  crypto,
27
32
  readSettingsFromDiskMigrated: async () => settings,
28
33
  writeSettingsToDisk: async (next) => { settings = next; },
29
- readSettingsStrict: async () => settings,
34
+ relayKeyStore,
30
35
  getLocalPort: () => 0,
31
36
  hasRelayDemand: options.hasRelayDemand ?? (async () => true),
32
37
  hostLock,
@@ -1,51 +1,28 @@
1
1
  // Per-server relay signing identity (ECDSA P-256), extracted from
2
2
  // lib/notifications/apns-runtime.js so both the push relay and the private
3
3
  // relay share the SAME keypair and thus the SAME serverId
4
- // (base64url(SHA-256(canonical public JWK))). Storage format is unchanged:
5
- // `settings.relaySigningKey = { privateJwk, publicJwk }` — existing installs'
6
- // serverId must stay stable because push token binding depends on it.
4
+ // (base64url(SHA-256(canonical public JWK))). Stored by key-store.js; existing
5
+ // installs' serverId must stay stable because push token binding depends on it.
7
6
 
8
7
  /**
9
8
  * @param {{
10
9
  * crypto: typeof import('node:crypto'),
11
- * readSettingsFromDiskMigrated: () => Promise<object>,
12
- * writeSettingsToDisk: (settings: object) => Promise<void>,
13
- * readSettingsStrict?: () => Promise<object>,
10
+ * relayKeyStore: ReturnType<typeof import('./key-store.js').createRelayKeyStore>,
14
11
  * }} deps
15
12
  * @returns {Promise<{ privateKey: import('node:crypto').KeyObject, publicJwk: JsonWebKey }>}
16
13
  */
17
- export const getOrCreateRelaySigningKeypair = async ({ crypto, readSettingsFromDiskMigrated, writeSettingsToDisk, readSettingsStrict }) => {
18
- const toKeypair = (stored) => ({
14
+ export const getOrCreateRelaySigningKeypair = async ({ crypto, relayKeyStore }) => {
15
+ const stored = await relayKeyStore.getOrCreate('signing', async () => {
16
+ // Loud on purpose: a new signing key means a new serverId — every previously
17
+ // paired device and push binding is orphaned. Expected exactly once, on first run.
18
+ console.warn('[relay-identity] Generating NEW relay signing keypair (serverId changes; previously paired devices must re-pair)');
19
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
20
+ return { privateJwk: privateKey.export({ format: 'jwk' }), publicJwk: publicKey.export({ format: 'jwk' }) };
21
+ });
22
+ return {
19
23
  privateKey: crypto.createPrivateKey({ key: stored.privateJwk, format: 'jwk' }),
20
24
  publicJwk: stored.publicJwk,
21
- });
22
- const settings = await readSettingsFromDiskMigrated();
23
- const existing = settings?.relaySigningKey;
24
- if (existing && existing.privateJwk && existing.publicJwk) {
25
- return toKeypair(existing);
26
- }
27
- // Regeneration gate: the lenient settings reader maps read failures to `{}`,
28
- // indistinguishable from "first run". Minting a new keypair changes serverId,
29
- // which orphans every paired device and push binding AND the write below would
30
- // clobber the settings file with the empty spread. Re-verify with the strict
31
- // reader (throws on corrupt/unreadable) before generating; if it finds the
32
- // key the lenient read lost, use it and generate nothing.
33
- let verifiedSettings = settings;
34
- if (readSettingsStrict) {
35
- verifiedSettings = await readSettingsStrict();
36
- const verified = verifiedSettings?.relaySigningKey;
37
- if (verified && verified.privateJwk && verified.publicJwk) {
38
- return toKeypair(verified);
39
- }
40
- }
41
- // Loud on purpose: a new signing key means a new serverId — every previously
42
- // paired device and push binding is orphaned. Expected exactly once, on first run.
43
- console.warn('[relay-identity] Generating NEW relay signing keypair (serverId changes; previously paired devices must re-pair)');
44
- const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
45
- const privateJwk = privateKey.export({ format: 'jwk' });
46
- const publicJwk = publicKey.export({ format: 'jwk' });
47
- await writeSettingsToDisk({ ...settings, ...(verifiedSettings || {}), relaySigningKey: { privateJwk, publicJwk } });
48
- return { privateKey, publicJwk };
25
+ };
49
26
  };
50
27
 
51
28
  // Fixed key order so the hash is stable regardless of stored JSON field order.
@@ -8,16 +8,16 @@
8
8
  import { MAX_PLAINTEXT_FRAME_BYTES } from './e2ee.js';
9
9
 
10
10
 
11
- export const TUNNEL_FRAME_HEADER_BYTES = 5;
12
- export const TUNNEL_FRAGMENT_FLAG = 0x80;
11
+ const TUNNEL_FRAME_HEADER_BYTES = 5;
12
+ const TUNNEL_FRAGMENT_FLAG = 0x80;
13
13
 
14
14
  // Batch envelope container (mirror of protocol.ts). Only used when both peers
15
15
  // negotiated `batch`. Reserve the per-frame envelope overhead from the payload
16
16
  // budget so any single frame still fits one 64 KiB encrypted plaintext.
17
- export const BATCH_CONTAINER_TAG_SINGLE = 0x00;
18
- export const BATCH_CONTAINER_TAG_BATCH = 0x01;
19
- export const BATCH_FRAME_LENGTH_BYTES = 4;
20
- export const BATCH_ENVELOPE_RESERVED_BYTES = 1 + BATCH_FRAME_LENGTH_BYTES;
17
+ const BATCH_CONTAINER_TAG_SINGLE = 0x00;
18
+ const BATCH_CONTAINER_TAG_BATCH = 0x01;
19
+ const BATCH_FRAME_LENGTH_BYTES = 4;
20
+ const BATCH_ENVELOPE_RESERVED_BYTES = 1 + BATCH_FRAME_LENGTH_BYTES;
21
21
  export const MAX_TUNNEL_PAYLOAD_BYTES =
22
22
  MAX_PLAINTEXT_FRAME_BYTES - TUNNEL_FRAME_HEADER_BYTES - BATCH_ENVELOPE_RESERVED_BYTES;
23
23
 
@@ -40,7 +40,7 @@ export const TunnelFrameType = {
40
40
  const TUNNEL_FRAME_TYPE_VALUES = new Set(Object.values(TunnelFrameType));
41
41
 
42
42
  /** @param {number} value */
43
- export const isTunnelFrameType = (value) => TUNNEL_FRAME_TYPE_VALUES.has(value);
43
+ const isTunnelFrameType = (value) => TUNNEL_FRAME_TYPE_VALUES.has(value);
44
44
 
45
45
  const MAX_STREAM_ID = 0xffffffff;
46
46
 
@@ -295,9 +295,9 @@ const BUFFERED_FRAME_TYPES = new Set([
295
295
 
296
296
  // See the TS mirror (tunnel-codec.ts) for the 150ms rationale: the chat render pipeline's
297
297
  // 100ms input throttle + ~64ms paced-reveal smoothing make a 150ms batch window invisible.
298
- export const DEFAULT_BATCH_WINDOW_MS = 150;
299
- export const DEFAULT_BATCH_MAX_BYTES = 24 * 1024;
300
- export const DEFAULT_BATCH_MAX_FRAMES = 32;
298
+ const DEFAULT_BATCH_WINDOW_MS = 150;
299
+ const DEFAULT_BATCH_MAX_BYTES = 24 * 1024;
300
+ const DEFAULT_BATCH_MAX_FRAMES = 32;
301
301
 
302
302
  /**
303
303
  * Outbound batching buffer (mirror of tunnel-codec.ts createOutboundFrameBatcher).
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ describe('lazy-loaded route module exports', () => {
4
+ it.each([
5
+ ['./git/index.js', [
6
+ 'computeIntegratePlan',
7
+ 'createBranch',
8
+ 'deleteProfile',
9
+ 'getCommitSummaries',
10
+ 'listStashes',
11
+ ]],
12
+ ['./quota/index.js', ['fetchQuotaForProvider', 'listConfiguredQuotaProviders']],
13
+ ['./tts/index.js', ['ttsService', 'TTS_VOICES']],
14
+ ['./walkthrough/index.js', ['getRepositoryRootFor']],
15
+ ])('%s keeps the exports its routes load dynamically', async (specifier, names) => {
16
+ const routeModule = await import(specifier);
17
+
18
+ for (const name of names) {
19
+ expect(routeModule[name], `${specifier} must export ${name}`).toBeDefined();
20
+ }
21
+ });
22
+ });
@@ -39,7 +39,11 @@ Auto. There is no env gate — the feature shipped dark behind
39
39
  - `history.js` — the last three settled turns through session assist's
40
40
  `loadAssistContext` (text parts only, attached quotes included, no files or
41
41
  tool payloads), each user message cut to its head and each answer to head
42
- plus tail. The new request is never cut.
42
+ plus tail. The new request is never cut. Also the model that wrote the last
43
+ settled answer and when it finished (`lastAnswer`), for `cache.js`.
44
+ - `cache.js` — "Try to preserve cache usage": which category a send stays on
45
+ while the current model's prompt cache is likely warm (`cacheHoldCategory`),
46
+ and how long that is given OpenCode's `warming` config (`warmWindowMs`).
43
47
  - `runtime.js` — `createRoutingRuntime`: `describe`, `classifierEndpoint` (the
44
48
  endpoint a Jev request goes to now, or null; also used by
45
49
  `../session-work` and `../session-goal`), `noteModelSelection`,
@@ -91,6 +95,21 @@ Auto. There is no env gate — the feature shipped dark behind
91
95
  switch, so the reply runs on the model's default thinking instead of OpenCode
92
96
  refusing it (Settings from before #4133 saved list positions like `"2"`).
93
97
  An unreadable catalog, or one that does not know the model yet, keeps it.
98
+ - A provider's prompt cache belongs to one model, so a model switch makes the
99
+ new model read the whole context uncached. With `preserveCache` on (the
100
+ default; a config without the key reads as on), a send Jev routes to a
101
+ built-in category below the one the session is on runs as that one instead,
102
+ while the cache is likely warm. "Below" is the built-in order trivial <
103
+ research < implement < hard; the session is on the highest enabled built-in
104
+ category whose model wrote the last settled answer. Moving up is never held,
105
+ and neither is a send routed to a user category, a fallback for low
106
+ confidence or an error, or a hold that would change the agent (another
107
+ agent is another system prompt, which misses the cache anyway). Warm means
108
+ within 5 minutes of the last answer finishing, or, when OpenCode's `warming`
109
+ is on for the directory, within its `duration` (30 minutes for `true`) plus
110
+ 5 minutes. The config is read only after the first 5 minutes; an unreadable
111
+ one counts as warming off. The decision event names the category the send
112
+ ran as and carries `cacheHold.requested`, the one Jev chose.
94
113
  - Auto is offered (`autoReady`) with a usable classification provider
95
114
  (`jevAvailable`), `enabled`, a fallback model and at least two enabled
96
115
  categories.
@@ -223,9 +242,10 @@ features that need Jev.
223
242
 
224
243
  ## Tests
225
244
 
226
- `store.test.js` (defaults, deviation round-trip, deleted built-ins, malformed
245
+ `store.test.js` (defaults, deviation round-trip, the cache setting's default,
246
+ deleted built-ins, malformed
227
247
  file, token file mode, classifier pick, custom endpoint file), `runtime.test.js` (request text,
228
- excerpts, decisions, endpoints, custom URL normalization and setter,
248
+ excerpts, decisions, cache holds and the warm window, endpoints, custom URL normalization and setter,
229
249
  classifier fallback, rewrite and fallback
230
250
  paths, safety net accept/hold/skip/unavailable), `routes.http.test.js`
231
251
  (sentinel dropped from a create and swallowed on the model switch, routed send