@vantran-se/openchamber-web 1.24.4 → 2.0.1

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -0,0 +1,57 @@
1
+ import path from 'node:path';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { packLocalTools } from './tools-pack.js';
6
+
7
+ const CHECKOUT = path.resolve('/checkout');
8
+ const OUT = path.resolve('/out');
9
+
10
+ /** A stand-in for bun. `pack` is what `bun pm pack --quiet` prints for a package. */
11
+ const fakeBun = ({ pack = (name) => `${path.join(OUT, `openchamber-${name}-1.24.2.tgz`)}\n`, failAt = () => false } = {}) => {
12
+ const calls = [];
13
+ const runCommand = async (file, args, options) => {
14
+ calls.push({ file, args, cwd: options.cwd, timeoutMs: options.timeoutMs });
15
+ if (failAt(args, options.cwd)) return { code: 1, stdout: '', stderr: 'error: Script not found "build"\n' };
16
+ return { code: 0, stdout: args[0] === 'pm' ? pack(path.basename(options.cwd)) : '', stderr: '' };
17
+ };
18
+ return { runCommand, calls };
19
+ };
20
+
21
+ describe('packLocalTools', () => {
22
+ it('builds the sdk, then packs the sdk and web from their own directories', async () => {
23
+ const bun = fakeBun();
24
+ const result = await packLocalTools({ runCommand: bun.runCommand, bunPath: '/opt/bun', checkoutRoot: CHECKOUT, outputDirectory: OUT });
25
+
26
+ expect(result).toEqual({
27
+ sdkTarballPath: path.join(OUT, 'openchamber-sdk-1.24.2.tgz'),
28
+ webTarballPath: path.join(OUT, 'openchamber-web-1.24.2.tgz'),
29
+ });
30
+ expect(bun.calls.map((call) => [call.file, call.args.join(' '), call.cwd])).toEqual([
31
+ ['/opt/bun', 'run build', path.join(CHECKOUT, 'packages', 'sdk')],
32
+ ['/opt/bun', `pm pack --destination ${OUT} --quiet`, path.join(CHECKOUT, 'packages', 'sdk')],
33
+ ['/opt/bun', `pm pack --destination ${OUT} --quiet`, path.join(CHECKOUT, 'packages', 'web')],
34
+ ]);
35
+ for (const call of bun.calls) expect(call.timeoutMs).toBeGreaterThan(0);
36
+ });
37
+
38
+ it('takes the last line as the tarball, because the prepack script prints before it', async () => {
39
+ const bun = fakeBun({ pack: (name) => `Built 0 built-in extension(s).\n${path.join(OUT, `openchamber-${name}-1.24.2.tgz`)}\n\n` });
40
+ const result = await packLocalTools({ runCommand: bun.runCommand, checkoutRoot: CHECKOUT, outputDirectory: OUT });
41
+ expect(result.webTarballPath).toBe(path.join(OUT, 'openchamber-web-1.24.2.tgz'));
42
+ });
43
+
44
+ it('says which step failed and what to do', async () => {
45
+ const bun = fakeBun({ failAt: (args) => args[0] === 'run' });
46
+ await expect(packLocalTools({ runCommand: bun.runCommand, checkoutRoot: CHECKOUT, outputDirectory: OUT })).rejects.toMatchObject({
47
+ code: 'tools_pack_failed',
48
+ message: expect.stringMatching(/build the sdk package.*Script not found.*bun install/s),
49
+ });
50
+ expect(bun.calls).toHaveLength(1);
51
+ });
52
+
53
+ it('rejects when bun does not name a tarball', async () => {
54
+ const bun = fakeBun({ pack: () => 'Done\n' });
55
+ await expect(packLocalTools({ runCommand: bun.runCommand, checkoutRoot: CHECKOUT, outputDirectory: OUT })).rejects.toMatchObject({ code: 'tools_pack_failed' });
56
+ });
57
+ });
@@ -0,0 +1,138 @@
1
+ // Tools sources: what goes into a tools volume. A source is plain data, made once.
2
+ //
3
+ // registry exact published versions, for a released host
4
+ // packed local tarballs of `web` and `sdk`, for a development build, whose
5
+ // packages differ from the published ones with the same version number
6
+ //
7
+ // Deciding which one a host needs is wiring and belongs to a later stage.
8
+
9
+ import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+
12
+ import { SpaceError } from './errors.js';
13
+ import { FILLER_PROGRAM } from './tools-filler.js';
14
+
15
+ // Where the filler puts the tarballs of a packed source. It is a tmpfs, so they vanish with the filler.
16
+ export const TOOLS_STAGING_PATH = '/tmp/openchamber-fill';
17
+
18
+ const WEB_PACKAGE_NAME = '@vantran-se/openchamber-web';
19
+ const WEB_TARBALL_NAME = 'openchamber-web.tgz';
20
+ const SDK_TARBALL_NAME = 'openchamber-sdk.tgz';
21
+
22
+ // Exact versions for the three packages this module names, never a range or a URL.
23
+ // That pins those three only. Their own dependencies are still ranges, and no lock file travels
24
+ // with the source, so two fills of one key can differ in a transitive package. A known limit.
25
+ const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
26
+
27
+ const requireVersion = (value, what) => {
28
+ if (!EXACT_VERSION_PATTERN.test(value ?? '')) {
29
+ throw new SpaceError('invalid_tools_version', `${what} needs an exact version such as 1.2.3, got '${value}'`);
30
+ }
31
+ return value;
32
+ };
33
+
34
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
35
+
36
+ const packageJsonText = (fields) => JSON.stringify({ name: 'openchamber-space-tools', private: true, ...fields });
37
+
38
+ // Every source made here. The Docker place accepts no other object as a source.
39
+ const madeHere = new WeakSet();
40
+
41
+ const finishSource = (source) => {
42
+ madeHere.add(Object.freeze(source));
43
+ return source;
44
+ };
45
+
46
+ // A revision changes the key and nothing else: the same packages, filled again into a new volume.
47
+ const REVISION_PATTERN = /^[A-Za-z0-9._-]{0,64}$/;
48
+
49
+ const requireRevision = (value) => {
50
+ if (!REVISION_PATTERN.test(value)) {
51
+ throw new SpaceError('invalid_tools_revision', 'A tools revision may hold only letters, digits, dots, dashes and underscores, 64 at most');
52
+ }
53
+ return value;
54
+ };
55
+
56
+ /** OpenCode and its plugin always share one version. */
57
+ const openCodePackages = (version) => ({ '@opencode/cli': version, '@opencode/plugin': version });
58
+
59
+ /**
60
+ * The versions a released host installs: its own web package, and OpenCode at the
61
+ * version of the `@opencode/client` this server was built against.
62
+ */
63
+ export function readHostToolVersions(packageJsonUrl = new URL('../../../package.json', import.meta.url)) {
64
+ let manifest;
65
+ try {
66
+ manifest = JSON.parse(fs.readFileSync(packageJsonUrl, 'utf8'));
67
+ } catch (error) {
68
+ throw new SpaceError('tools_versions_unreadable', `Could not read the package.json of the OpenChamber server: ${error.message}`);
69
+ }
70
+ return {
71
+ webVersion: requireVersion(manifest.version, WEB_PACKAGE_NAME),
72
+ openCodeVersion: requireVersion(manifest.dependencies?.['@opencode/client'], 'OpenCode'),
73
+ };
74
+ }
75
+
76
+ export function createRegistryToolsSource({ webVersion, openCodeVersion, revision = '' }) {
77
+ const packages = {
78
+ [WEB_PACKAGE_NAME]: requireVersion(webVersion, WEB_PACKAGE_NAME),
79
+ ...openCodePackages(requireVersion(openCodeVersion, 'OpenCode')),
80
+ };
81
+ return finishSource({
82
+ kind: 'registry',
83
+ description: `web ${webVersion}, opencode ${openCodeVersion}`,
84
+ canonical: { kind: 'registry', packages, revision: requireRevision(revision) },
85
+ packageJson: packageJsonText({ dependencies: packages }),
86
+ files: [],
87
+ });
88
+ }
89
+
90
+ /**
91
+ * A development build. The two tarballs are read and hashed here, once.
92
+ * The override makes every `@openchamber/sdk` in the tree the local tarball, also the one
93
+ * that `web` asks for by version number.
94
+ */
95
+ export function createPackedToolsSource({ webTarballPath, sdkTarballPath, openCodeVersion, revision = '' }) {
96
+ const read = (filePath, what) => {
97
+ try {
98
+ return fs.readFileSync(filePath);
99
+ } catch (error) {
100
+ throw new SpaceError('tools_tarball_unreadable', `Could not read the packed ${what} package at ${filePath}: ${error.message}`);
101
+ }
102
+ };
103
+ const files = [
104
+ { name: WEB_TARBALL_NAME, bytes: read(webTarballPath, 'web') },
105
+ { name: SDK_TARBALL_NAME, bytes: read(sdkTarballPath, 'sdk') },
106
+ ];
107
+ const packages = openCodePackages(requireVersion(openCodeVersion, 'OpenCode'));
108
+ return finishSource({
109
+ kind: 'packed',
110
+ description: 'development build',
111
+ canonical: { kind: 'packed', tarballs: files.map((file) => ({ name: file.name, sha256: sha256(file.bytes) })), packages, revision: requireRevision(revision) },
112
+ packageJson: packageJsonText({
113
+ dependencies: {
114
+ '@openchamber/web': `file:${TOOLS_STAGING_PATH}/${WEB_TARBALL_NAME}`,
115
+ '@openchamber/sdk': `file:${TOOLS_STAGING_PATH}/${SDK_TARBALL_NAME}`,
116
+ ...packages,
117
+ },
118
+ overrides: { '@openchamber/sdk': '$@openchamber/sdk' },
119
+ }),
120
+ files,
121
+ });
122
+ }
123
+
124
+ export function requireToolsSource(source) {
125
+ if (!madeHere.has(source)) {
126
+ throw new SpaceError('invalid_tools_source', 'The Docker place needs a tools source from createRegistryToolsSource or createPackedToolsSource');
127
+ }
128
+ return source;
129
+ }
130
+
131
+ /**
132
+ * First 16 hex characters of a sha256 over everything that decides the content of a tools
133
+ * volume: the source, the base image the filler runs in, and the filler program itself.
134
+ */
135
+ export function toolsContentKey(source, image) {
136
+ const description = JSON.stringify({ source: requireToolsSource(source).canonical, image, filler: sha256(FILLER_PROGRAM) });
137
+ return sha256(description).slice(0, 16);
138
+ }
@@ -0,0 +1,153 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8
+
9
+ import { createPackedToolsSource, createRegistryToolsSource, readHostToolVersions, requireToolsSource, toolsContentKey } from './tools.js';
10
+
11
+ const IMAGE = 'node@sha256:aaaa';
12
+ const VERSIONS = { webVersion: '1.24.2', openCodeVersion: '1.18.31' };
13
+
14
+ let directory;
15
+ const tarball = (name, content) => {
16
+ const filePath = path.join(directory, name);
17
+ fs.writeFileSync(filePath, content);
18
+ return filePath;
19
+ };
20
+
21
+ beforeAll(() => {
22
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-tools-test-'));
23
+ });
24
+
25
+ afterAll(() => {
26
+ fs.rmSync(directory, { recursive: true, force: true });
27
+ });
28
+
29
+ describe('readHostToolVersions', () => {
30
+ it('reads the version of this server and the OpenCode version it was built against', () => {
31
+ const versions = readHostToolVersions();
32
+ expect(versions.webVersion).toMatch(/^\d+\.\d+\.\d+/);
33
+ expect(versions.openCodeVersion).toMatch(/^\d+\.\d+\.\d+/);
34
+ });
35
+
36
+ it('rejects a manifest whose OpenCode dependency is a range', () => {
37
+ const manifest = tarball('range.json', JSON.stringify({ version: '1.0.0', dependencies: { '@opencode/client': '^1.18.0' } }));
38
+ expect(() => readHostToolVersions(pathToFileURL(manifest))).toThrow(expect.objectContaining({ code: 'invalid_tools_version' }));
39
+ });
40
+
41
+ it('says when the manifest cannot be read', () => {
42
+ expect(() => readHostToolVersions(pathToFileURL(path.join(directory, 'missing.json')))).toThrow(expect.objectContaining({ code: 'tools_versions_unreadable' }));
43
+ });
44
+ });
45
+
46
+ describe('createRegistryToolsSource', () => {
47
+ it('asks for the fork package at the exact host version', () => {
48
+ const source = createRegistryToolsSource(VERSIONS);
49
+
50
+ expect(source.description).toBe('web 1.24.2, opencode 1.18.31');
51
+ expect(source.files).toEqual([]);
52
+ expect(JSON.parse(source.packageJson)).toEqual({
53
+ name: 'openchamber-space-tools',
54
+ private: true,
55
+ dependencies: { '@vantran-se/openchamber-web': '1.24.2', '@opencode/cli': '1.18.31', '@opencode/plugin': '1.18.31' },
56
+ });
57
+ });
58
+
59
+ it.each(['^1.24.2', 'latest', '1.24', '', undefined, 'https://example.com/x.tgz', '1.24.2 || 2'])('rejects the version %j', (version) => {
60
+ expect(() => createRegistryToolsSource({ ...VERSIONS, webVersion: version })).toThrow(expect.objectContaining({ code: 'invalid_tools_version' }));
61
+ expect(() => createRegistryToolsSource({ ...VERSIONS, openCodeVersion: version })).toThrow(expect.objectContaining({ code: 'invalid_tools_version' }));
62
+ });
63
+
64
+ it('accepts a prerelease version', () => {
65
+ expect(createRegistryToolsSource({ ...VERSIONS, webVersion: '1.25.0-beta.1' }).description).toContain('1.25.0-beta.1');
66
+ });
67
+ });
68
+
69
+ describe('createPackedToolsSource', () => {
70
+ it('installs both tarballs by path and makes every sdk in the tree the local one', () => {
71
+ const source = createPackedToolsSource({ webTarballPath: tarball('web.tgz', 'web bytes'), sdkTarballPath: tarball('sdk.tgz', 'sdk bytes'), openCodeVersion: '1.18.31' });
72
+
73
+ expect(source.description).toBe('development build');
74
+ expect(source.files).toEqual([
75
+ { name: 'openchamber-web.tgz', bytes: Buffer.from('web bytes') },
76
+ { name: 'openchamber-sdk.tgz', bytes: Buffer.from('sdk bytes') },
77
+ ]);
78
+ expect(JSON.parse(source.packageJson)).toEqual({
79
+ name: 'openchamber-space-tools',
80
+ private: true,
81
+ dependencies: {
82
+ '@openchamber/web': 'file:/tmp/openchamber-fill/openchamber-web.tgz',
83
+ '@openchamber/sdk': 'file:/tmp/openchamber-fill/openchamber-sdk.tgz',
84
+ '@opencode/cli': '1.18.31',
85
+ '@opencode/plugin': '1.18.31',
86
+ },
87
+ overrides: { '@openchamber/sdk': '$@openchamber/sdk' },
88
+ });
89
+ });
90
+
91
+ it('hashes the tarballs once, when the source is made', () => {
92
+ const webTarballPath = tarball('web-once.tgz', 'first');
93
+ const source = createPackedToolsSource({ webTarballPath, sdkTarballPath: tarball('sdk-once.tgz', 'sdk'), openCodeVersion: '1.18.31' });
94
+ const key = toolsContentKey(source, IMAGE);
95
+
96
+ fs.writeFileSync(webTarballPath, 'changed later');
97
+ expect(toolsContentKey(source, IMAGE)).toBe(key);
98
+ expect(source.canonical.tarballs[0].sha256).toBe(crypto.createHash('sha256').update('first').digest('hex'));
99
+ });
100
+
101
+ it('says which tarball it could not read', () => {
102
+ const missing = path.join(directory, 'nowhere.tgz');
103
+ expect(() => createPackedToolsSource({ webTarballPath: missing, sdkTarballPath: missing, openCodeVersion: '1.18.31' }))
104
+ .toThrow(expect.objectContaining({ code: 'tools_tarball_unreadable', message: expect.stringContaining('web') }));
105
+ });
106
+ });
107
+
108
+ describe('toolsContentKey', () => {
109
+ const packed = (webContent) => createPackedToolsSource({ webTarballPath: tarball(`web-${webContent}.tgz`, webContent), sdkTarballPath: tarball('sdk-key.tgz', 'sdk'), openCodeVersion: '1.18.31' });
110
+
111
+ it('is 16 hex characters and the same for the same content', () => {
112
+ const key = toolsContentKey(createRegistryToolsSource(VERSIONS), IMAGE);
113
+ expect(key).toMatch(/^[0-9a-f]{16}$/);
114
+ expect(toolsContentKey(createRegistryToolsSource(VERSIONS), IMAGE)).toBe(key);
115
+ });
116
+
117
+ it('changes with a version, with the base image, with a tarball byte, and between the two kinds', () => {
118
+ const keys = [
119
+ toolsContentKey(createRegistryToolsSource(VERSIONS), IMAGE),
120
+ toolsContentKey(createRegistryToolsSource({ ...VERSIONS, webVersion: '1.24.3' }), IMAGE),
121
+ toolsContentKey(createRegistryToolsSource({ ...VERSIONS, openCodeVersion: '1.18.32' }), IMAGE),
122
+ toolsContentKey(createRegistryToolsSource(VERSIONS), 'node@sha256:bbbb'),
123
+ toolsContentKey(packed('a'), IMAGE),
124
+ toolsContentKey(packed('b'), IMAGE),
125
+ toolsContentKey(createRegistryToolsSource({ ...VERSIONS, revision: 'refill-1' }), IMAGE),
126
+ ];
127
+ expect(new Set(keys).size).toBe(keys.length);
128
+ });
129
+ });
130
+
131
+ describe('revision', () => {
132
+ it('changes the key and nothing that gets installed', () => {
133
+ const plain = createRegistryToolsSource(VERSIONS);
134
+ const revised = createRegistryToolsSource({ ...VERSIONS, revision: 'refill-1' });
135
+ expect(revised.packageJson).toBe(plain.packageJson);
136
+ expect(revised.description).toBe(plain.description);
137
+ });
138
+
139
+ it.each(['two words', 'a/b', 'x'.repeat(65)])('rejects %j', (revision) => {
140
+ expect(() => createRegistryToolsSource({ ...VERSIONS, revision })).toThrow(expect.objectContaining({ code: 'invalid_tools_revision' }));
141
+ });
142
+ });
143
+
144
+ describe('requireToolsSource', () => {
145
+ it.each([undefined, null, 'registry', {}, { ...createRegistryToolsSource(VERSIONS) }])('rejects %j, which no factory here made', (source) => {
146
+ expect(() => requireToolsSource(source)).toThrow(expect.objectContaining({ code: 'invalid_tools_source' }));
147
+ });
148
+
149
+ it('accepts a source from either factory', () => {
150
+ const source = createRegistryToolsSource(VERSIONS);
151
+ expect(requireToolsSource(source)).toBe(source);
152
+ });
153
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The OpenAI SDK is ~200 modules and only speech and transcription use it, so
3
+ * it is loaded on the first call rather than with the server.
4
+ */
5
+
6
+ let pending;
7
+
8
+ export const loadOpenAI = () => {
9
+ pending ??= import('openai').then((module) => module.default);
10
+ return pending;
11
+ };
@@ -5,8 +5,8 @@
5
5
  * This bypasses mobile Safari's audio context restrictions.
6
6
  */
7
7
 
8
- import OpenAI from 'openai';
9
8
  import { readAuthFile } from '../opencode/auth.js';
9
+ import { loadOpenAI } from './openai-sdk.js';
10
10
  import { normalizeCustomOpenAIBaseURL } from './base-url.js';
11
11
 
12
12
  // Voice options from OpenAI
@@ -53,11 +53,12 @@ class TTSService {
53
53
  this._lastApiKey = null;
54
54
  }
55
55
 
56
- _getClient() {
56
+ async _getClient() {
57
57
  const apiKey = getOpenAIApiKey();
58
58
 
59
59
  // If API key changed or client doesn't exist, create new client
60
60
  if (apiKey && (!this._client || this._lastApiKey !== apiKey)) {
61
+ const OpenAI = await loadOpenAI();
61
62
  this._client = new OpenAI({ apiKey });
62
63
  this._lastApiKey = apiKey;
63
64
  }
@@ -66,7 +67,7 @@ class TTSService {
66
67
  }
67
68
 
68
69
  isAvailable() {
69
- return this._getClient() !== null;
70
+ return Boolean(getOpenAIApiKey());
70
71
  }
71
72
 
72
73
  /**
@@ -96,9 +97,10 @@ class TTSService {
96
97
  if (apiKey) clientOpts.apiKey = apiKey;
97
98
  if (!apiKey) clientOpts.apiKey = 'not-required';
98
99
  if (normalizedBaseURL) clientOpts.baseURL = normalizedBaseURL;
100
+ const OpenAI = await loadOpenAI();
99
101
  client = new OpenAI(clientOpts);
100
102
  } else {
101
- client = this._getClient();
103
+ client = await this._getClient();
102
104
  }
103
105
 
104
106
  if (!client) {
@@ -141,7 +143,7 @@ class TTSService {
141
143
  * Generate speech and return as a buffer (for caching)
142
144
  */
143
145
  async generateSpeechBuffer(options) {
144
- const client = this._getClient();
146
+ const client = await this._getClient();
145
147
  if (!client) {
146
148
  throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable or configure OpenAI in OpenCode.');
147
149
  }
@@ -5,7 +5,7 @@
5
5
  * (e.g. faster-whisper, whisper.cpp) using the OpenAI Node SDK.
6
6
  */
7
7
 
8
- import OpenAI, { toFile } from 'openai';
8
+ import { loadOpenAI } from './openai-sdk.js';
9
9
  import { normalizeCustomOpenAIBaseURL } from './base-url.js';
10
10
 
11
11
  /**
@@ -36,6 +36,7 @@ export async function transcribeAudio({ audioBuffer, mimeType, model, baseURL, a
36
36
  };
37
37
  clientOpts.baseURL = normalizedBaseURL;
38
38
 
39
+ const OpenAI = await loadOpenAI();
39
40
  const client = new OpenAI(clientOpts);
40
41
 
41
42
  // Derive a sensible filename extension from the MIME type so the server
@@ -43,7 +44,7 @@ export async function transcribeAudio({ audioBuffer, mimeType, model, baseURL, a
43
44
  const ext = mimeTypeToExt(mimeType);
44
45
  const filename = `audio.${ext}`;
45
46
 
46
- const file = await toFile(audioBuffer, filename, { type: mimeType });
47
+ const file = await OpenAI.toFile(audioBuffer, filename, { type: mimeType });
47
48
 
48
49
  const result = await client.audio.transcriptions.create({
49
50
  file,
@@ -3,6 +3,8 @@
3
3
  ## Purpose
4
4
  This module contains tunnel provider orchestration for OpenChamber, including provider registry/service wiring, managed remote token config lifecycle, and tunnel HTTP route registration.
5
5
 
6
+ Public tunnels require a UI password on the running server. The shared tunnel start operation enforces this for startup tunnels and HTTP requests; the HTTP route rejects passwordless starts before touching an existing tunnel.
7
+
6
8
  ## Entrypoints and structure
7
9
  - `packages/web/server/lib/tunnels/index.js`: tunnel service orchestration.
8
10
  - `packages/web/server/lib/tunnels/executable-search.js`: cross-platform executable discovery, including Windows Store app aliases.
@@ -5,6 +5,7 @@ export const createTunnelRoutesRuntime = (dependencies) => {
5
5
  tunnelService,
6
6
  tunnelProviderRegistry,
7
7
  tunnelAuthController,
8
+ hasUiPassword,
8
9
  readSettingsFromDiskMigrated,
9
10
  readManagedRemoteTunnelConfigFromDisk,
10
11
  normalizeTunnelProvider,
@@ -74,6 +75,9 @@ export const createTunnelRoutesRuntime = (dependencies) => {
74
75
  selectedPresetId,
75
76
  selectedPresetName,
76
77
  }) => {
78
+ if (!hasUiPassword) {
79
+ throw new Error('A UI password is required before starting a public tunnel. Restart OpenChamber with --ui-password.');
80
+ }
77
81
  if (provider === TUNNEL_PROVIDER_CLOUDFLARE && mode === TUNNEL_MODE_MANAGED_REMOTE) {
78
82
  setRuntimeManagedRemoteTunnelHostname(hostname);
79
83
  setRuntimeManagedRemoteTunnelToken(token);
@@ -445,6 +449,9 @@ export const createTunnelRoutesRuntime = (dependencies) => {
445
449
  });
446
450
 
447
451
  app.post('/api/openchamber/tunnel/start', async (_req, res) => {
452
+ if (!hasUiPassword) {
453
+ return res.status(403).json({ ok: false, code: 'ui_password_required', error: 'A UI password is required before starting a public tunnel. Restart OpenChamber with --ui-password.' });
454
+ }
448
455
  try {
449
456
  const settings = await readSettingsFromDiskMigrated();
450
457
  if (typeof _req?.body?.provider === 'string' && _req.body.provider.trim().length > 0) {
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createTunnelRoutesRuntime } from './routes.js';
3
+
4
+ describe('public tunnel password requirement', () => {
5
+ const createRuntime = (hasUiPassword) => {
6
+ let starts = 0;
7
+ const runtime = createTunnelRoutesRuntime({
8
+ hasUiPassword,
9
+ tunnelService: {
10
+ start: async () => {
11
+ starts += 1;
12
+ return { provider: 'cloudflare', publicUrl: 'https://example.com', activeMode: 'quick' };
13
+ },
14
+ },
15
+ TUNNEL_PROVIDER_CLOUDFLARE: 'cloudflare',
16
+ TUNNEL_MODE_MANAGED_REMOTE: 'managed-remote',
17
+ });
18
+ return { runtime, getStarts: () => starts };
19
+ };
20
+
21
+ it('rejects an unauthenticated runtime before starting any provider', async () => {
22
+ const { runtime, getStarts } = createRuntime(false);
23
+ await expect(runtime.startTunnelWithNormalizedRequest({ provider: 'cloudflare', mode: 'quick' }))
24
+ .rejects.toThrow('A UI password is required');
25
+ expect(getStarts()).toBe(0);
26
+ });
27
+
28
+ it('rejects the HTTP start route without disturbing an existing tunnel', async () => {
29
+ const { runtime, getStarts } = createRuntime(false);
30
+ const routes = new Map();
31
+ const app = {
32
+ get: (path, handler) => routes.set(`GET ${path}`, handler),
33
+ post: (path, handler) => routes.set(`POST ${path}`, handler),
34
+ put: (path, handler) => routes.set(`PUT ${path}`, handler),
35
+ };
36
+ runtime.registerRoutes(app);
37
+ let status = 200;
38
+ let body;
39
+ const response = {
40
+ status(code) { status = code; return this; },
41
+ json(payload) { body = payload; return this; },
42
+ };
43
+ await routes.get('POST /api/openchamber/tunnel/start')({ body: {} }, response);
44
+ expect(status).toBe(403);
45
+ expect(body.code).toBe('ui_password_required');
46
+ expect(getStarts()).toBe(0);
47
+ });
48
+
49
+ it('allows a password-protected runtime to start a tunnel', async () => {
50
+ const { runtime, getStarts } = createRuntime(true);
51
+ const result = await runtime.startTunnelWithNormalizedRequest({ provider: 'cloudflare', mode: 'quick' });
52
+ expect(result.publicUrl).toBe('https://example.com');
53
+ expect(getStarts()).toBe(1);
54
+ });
55
+ });
@@ -1,5 +1,11 @@
1
1
  import crypto from 'crypto';
2
- import { SignJWT, jwtVerify } from 'jose';
2
+
3
+ // jose is loaded on the first session check, not with the server.
4
+ let josePending;
5
+ const loadJose = () => {
6
+ josePending ??= import('jose');
7
+ return josePending;
8
+ };
3
9
  import fs from 'fs';
4
10
  import path from 'path';
5
11
  import os from 'os';
@@ -267,7 +273,34 @@ const getUrlAuthTokenFromRequest = (req) => {
267
273
  token = undefined;
268
274
  }
269
275
  }
270
- return typeof token === 'string' && token.trim() ? token.trim() : null;
276
+ if (typeof token === 'string' && token.trim()) return token.trim();
277
+ return getServedPageSubresourceToken(req);
278
+ };
279
+
280
+ /**
281
+ * An HTML file shown through `/api/fs/serve/` loads its own images, styles
282
+ * and scripts with plain relative URLs, which carry no token: only the page's
283
+ * URL has one. The browser sends that page URL as the Referer of every
284
+ * same-origin subresource request, so the page's token is taken from there,
285
+ * and only when both the page and the subresource live under `/api/fs/serve/`.
286
+ * Nothing else about the token changes: it is still checked for validity,
287
+ * expiry and scope like a query token.
288
+ */
289
+ const SERVED_PAGE_PREFIX = '/api/fs/serve/';
290
+
291
+ const getServedPageSubresourceToken = (req) => {
292
+ if (!getRequestPathname(req).startsWith(SERVED_PAGE_PREFIX)) return null;
293
+ const referer = req?.headers?.referer;
294
+ const value = Array.isArray(referer) ? referer[0] : referer;
295
+ if (typeof value !== 'string' || !value) return null;
296
+ try {
297
+ const url = new URL(value);
298
+ if (!url.pathname.startsWith(SERVED_PAGE_PREFIX)) return null;
299
+ const token = url.searchParams.get('oc_url_token');
300
+ return token && token.trim() ? token.trim() : null;
301
+ } catch {
302
+ return null;
303
+ }
271
304
  };
272
305
 
273
306
  const getRequestPathname = (req) => {
@@ -333,6 +366,7 @@ const isUrlAuthWebSocketPath = (pathname) => {
333
366
  || pathname === '/api/terminal/ws'
334
367
  || pathname === '/api/dictation/ws'
335
368
  || pathname === '/api/dev-tunnel'
369
+ || /^\/api\/guests\/[a-z][a-z0-9-]*\/surface\/ws$/.test(pathname)
336
370
  || pathname.startsWith('/api/preview/proxy/');
337
371
  };
338
372
 
@@ -726,6 +760,7 @@ export const createUiAuth = ({
726
760
  return false;
727
761
  }
728
762
  try {
763
+ const { jwtVerify } = await loadJose();
729
764
  await jwtVerify(token, jwtSecret);
730
765
  return true;
731
766
  } catch {
@@ -735,6 +770,7 @@ export const createUiAuth = ({
735
770
 
736
771
  const issueSession = async (req, res, { trustDevice = false } = {}) => {
737
772
  const ttlMs = resolveSessionTtlMs(trustDevice);
773
+ const { SignJWT } = await loadJose();
738
774
  const token = await new SignJWT({ type: 'ui-session' })
739
775
  .setProtectedHeader({ alg: 'HS256' })
740
776
  .setIssuedAt()
@@ -190,6 +190,32 @@ describe('ui auth client credential seam', () => {
190
190
  });
191
191
  expect(serveCalled).toBe(true);
192
192
 
193
+ // A served page's own images carry no token; the page URL in the Referer
194
+ // stands in, but only between two paths under /api/fs/serve/.
195
+ const pageUrl = `http://127.0.0.1:3001/api/fs/serve/tmp/index.html?oc_url_token=${encodeURIComponent(urlToken)}`;
196
+ const subresourceReq = { method: 'GET', path: '/api/fs/serve/tmp/images/photo.png', url: '/api/fs/serve/tmp/images/photo.png', headers: { referer: pageUrl } };
197
+ const subresourceRes = createResponse();
198
+ let subresourceCalled = false;
199
+ await auth.requireAuth(subresourceReq, subresourceRes, () => {
200
+ subresourceCalled = true;
201
+ });
202
+ expect(subresourceCalled).toBe(true);
203
+
204
+ for (const denied of [
205
+ // The same referer must not open anything outside the served tree.
206
+ { method: 'GET', path: '/api/fs/raw', url: '/api/fs/raw?path=%2Ftmp%2Fsecret.png', headers: { referer: pageUrl } },
207
+ // A referer that is not a served page opens nothing.
208
+ { method: 'GET', path: '/api/fs/serve/tmp/images/photo.png', url: '/api/fs/serve/tmp/images/photo.png', headers: { referer: `http://127.0.0.1:3001/?oc_url_token=${encodeURIComponent(urlToken)}` } },
209
+ ]) {
210
+ const deniedRes = createResponse();
211
+ let deniedCalled = false;
212
+ await auth.requireAuth(denied, deniedRes, () => {
213
+ deniedCalled = true;
214
+ });
215
+ expect(deniedCalled).toBe(false);
216
+ expect(deniedRes.statusCode).toBe(401);
217
+ }
218
+
193
219
  const absoluteServeReq = { method: 'GET', path: '/api/fs/serve/Users/test/project/preview-test.html', url: `/api/fs/serve/Users/test/project/preview-test.html?oc_url_token=${encodeURIComponent(urlToken)}`, headers: {} };
194
220
  const absoluteServeRes = createResponse();
195
221
  let absoluteServeCalled = false;
@@ -261,6 +287,22 @@ describe('ui auth client credential seam', () => {
261
287
  };
262
288
  expect(await auth.ensureSessionToken(dictationWsReq, null)).toBe('client:device-1');
263
289
 
290
+ // An extension surface socket takes the session-wide URL token, never a
291
+ // guest-scoped one: it carries the user's pointer and keyboard.
292
+ const surfaceWsReq = {
293
+ method: 'GET',
294
+ path: '/api/guests/server-chrome/surface/ws',
295
+ url: `/api/guests/server-chrome/surface/ws?oc_url_token=${encodeURIComponent(urlToken)}`,
296
+ headers: { upgrade: 'websocket' },
297
+ };
298
+ expect(await auth.ensureSessionToken(surfaceWsReq, null)).toBe('client:device-1');
299
+ expect(await auth.ensureSessionToken({ ...surfaceWsReq, url: `/api/guests/server-chrome/surface/ws?oc_url_token=${encodeURIComponent(guestToken)}` }, null)).toBe(null);
300
+ expect(await auth.ensureSessionToken({
301
+ ...surfaceWsReq,
302
+ path: '/api/guests/server-chrome/surface/ws/extra',
303
+ url: `/api/guests/server-chrome/surface/ws/extra?oc_url_token=${encodeURIComponent(urlToken)}`,
304
+ }, null)).toBe(null);
305
+
264
306
  const devTunnelWsReq = {
265
307
  method: 'GET',
266
308
  path: '/api/dev-tunnel',