@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.
- package/README.md +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- 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
|
|
8
|
-
const
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
22
|
-
const runtime = createRelayIdentityRuntime({ crypto,
|
|
18
|
+
const relayKeyStore = makeKeyStore();
|
|
19
|
+
const runtime = createRelayIdentityRuntime({ crypto, relayKeyStore });
|
|
23
20
|
const identity = await runtime.getRelayIdentity();
|
|
24
21
|
|
|
25
|
-
const stored =
|
|
26
|
-
expect(stored.
|
|
27
|
-
expect(stored.
|
|
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.
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
71
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8
|
-
//
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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))).
|
|
5
|
-
//
|
|
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
|
-
*
|
|
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,
|
|
18
|
-
const
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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,
|
|
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
|