@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
|
@@ -17,6 +17,7 @@ This module provides Git repository operations for the web server runtime, inclu
|
|
|
17
17
|
- `transport-config.js`: bounded effective Git transport and checkout-hydration configuration hashing for repository authority.
|
|
18
18
|
- `submodule-discovery.js`, `lfs-discovery.js`, and `discovery-endpoint.js`: bounded pure parsers for committed checkout metadata and endpoint resolution.
|
|
19
19
|
- `contributor-provenance-storage.js`: strict bounded persistence for contributor-fork worktree identity and push-safety authority.
|
|
20
|
+
- `worktree-creation.js`: worktree creation with the server's authority checks, including the change-request pipeline (resolve, exact account, managed head transfer, contributor remote and provenance). `POST /api/git/worktrees` and the session service (`session.create` with `pullRequest`) both create through it; `registerGitRoutes` returns its `createWorktree` for that.
|
|
20
21
|
- `credential-resolver.js` and `credential-broker.js`: resolve opaque managed credential references and provide one-operation Git authentication without returning secrets to clients.
|
|
21
22
|
- `redaction.js`: bounded Git output and error redaction.
|
|
22
23
|
- `helper-launch.js`: how Git reaches this module's helper scripts through `sh`, telling Electron to run as Node inside the command itself.
|
|
@@ -42,8 +43,10 @@ Author-profile application never sets or unsets `core.sshCommand` or `credential
|
|
|
42
43
|
An identity is one repository's complete answer: signature, provider account (`account: { provider, instance, accountId }`), and transport (`account`, `ssh` with `sshCredentialId`, or `anonymous`). A client cannot write an identity without an account or with the `system` transport; records written before identities carried an account are still read (absent transport means `system`) but no repository can choose them. Connecting an account creates its identity on the spot, named after the login and qualified by provider or host when that name is already taken — one person with the same username on GitHub and GitLab gets `ada` and `ada (GitLab)`, not `ada 2`. Connecting again after disconnecting reuses the identity that already carries that person's signature on that instance rather than minting a second one (GitHub without a public address gets its own no-reply address); accounts connected earlier are backfilled once, on the first identities read after startup rather than at route registration, so a slow or locked auth store cannot delay server start. The public profile DTO contains only `id`, `name`, `userName`, `userEmail`, `account`, `transport`, `sshCredentialId`, `signCommits`, `signingKey`, `color`, and `icon`. Renderer Git summary routes expose only author name/email, and remote routes expose redacted display URLs. Raw `core.sshCommand` values and remote credentials, query parameters, and fragments remain server-internal.
|
|
43
44
|
|
|
44
45
|
### Status and Diff Operations
|
|
45
|
-
- `getStatus(directory, { mode })`: Get comprehensive Git status including current branch, tracking, ahead/behind, file changes, diff stats, merge/rebase state. `mode: 'light'` skips the diff stats. One read runs per directory at a time and at most four run across directories (`serial-refresh.js`): a call made while a read is running waits for one follow-up read that starts after the call, so no caller gets a snapshot older than its request, and every caller that arrives during one read shares that single follow-up at the widest mode any of them asked for. Clients refresh after every completed agent tool call and from several surfaces at once; on a large repository (a status read is a dozen Git processes walking the working tree) this bound is what keeps identical `git status` processes from piling up side by side. A slot is held only while the read is alive: every process the read spawns is killed after two minutes without output (one minute for the untracked-directory listing, thirty seconds for the repository probe), so a Git process that hangs, which happens on Windows, fails that read instead of holding a slot until someone kills it by hand. On Windows the listing is ended with `taskkill /T`, because the spawned `git.exe` is Git for Windows' launcher and killing it alone leaves the real `git` child walking the tree as an orphan. Untracked files are listed with `-unormal` and each new directory is then expanded to its files with a bounded `ls-files` listing (`UNTRACKED_DIRECTORY_EXPANSION_LIMIT`, 1000): up to that many files the result equals `-uall`; beyond it the directory stays one `dir/` entry, because `-uall` would walk a forgotten build or dependency directory in full on every read. A nested repository stays a `dir/` entry as before.
|
|
46
|
+
- `getStatus(directory, { mode })`: Get comprehensive Git status including current branch, tracking, ahead/behind, file changes, diff stats, merge/rebase state. `mode: 'light'` skips the diff stats. One read runs per directory at a time and at most four run across directories (`serial-refresh.js`): a call made while a read is running waits for one follow-up read that starts after the call, so no caller gets a snapshot older than its request, and every caller that arrives during one read shares that single follow-up at the widest mode any of them asked for. Clients refresh after every completed agent tool call and from several surfaces at once; on a large repository (a status read is a dozen Git processes walking the working tree) this bound is what keeps identical `git status` processes from piling up side by side. A slot is held only while the read is alive: every process the read spawns is killed after two minutes without output (one minute for the untracked-directory listing, thirty seconds for the repository probe), so a Git process that hangs, which happens on Windows, fails that read instead of holding a slot until someone kills it by hand. On Windows the listing is ended with `taskkill /T`, because the spawned `git.exe` is Git for Windows' launcher and killing it alone leaves the real `git` child walking the tree as an orphan. Untracked files are listed with `-unormal` and each new directory is then expanded to its files with a bounded `ls-files` listing (`UNTRACKED_DIRECTORY_EXPANSION_LIMIT`, 1000): up to that many files the result equals `-uall`; beyond it the directory stays one `dir/` entry, because `-uall` would walk a forgotten build or dependency directory in full on every read. A nested repository stays a `dir/` entry as before. A status read never writes the index (#2229): its commands run with `GIT_OPTIONAL_LOCKS=0`, and the working-tree stats come from `git diff-files -M --numstat`, because `git diff` rewrites a stale index even with that variable set. Otherwise each read holds `.git/index.lock` for as long as refreshing the index takes, and a `git commit` the user runs in a terminal at that moment fails with "index.lock: File exists". The cost is that the server never refreshes stale index timestamps itself, so after many files are touched without changes each read compares their contents until a command the user runs refreshes the index.
|
|
46
47
|
- `getTrackingBranch(directory)`: Upstream of the checked-out branch as `remote/branch` (the same value as `status.tracking`, including an upstream whose remote ref is gone), or `null` when HEAD is detached or unborn or no upstream is configured. Reads refs and config only. Callers that need just the tracking name (GitHub PR status polling, PR creation) use this instead of `getStatus`.
|
|
48
|
+
- `isAncestorOfHead(directory, sha)`: Check whether a commit is reachable from the checked-out `HEAD`.
|
|
49
|
+
- `isPatchEquivalentOfHead(directory, sha)`: Check whether a commit has a patch-equivalent commit reachable from `HEAD`, for terminal PR history after a rebase. It runs a short bounded `git cherry` probe. Missing old objects cannot prove an association.
|
|
47
50
|
- `getDiff(directory, { path, staged, contextLines })`: Get diff output for files or entire working tree with full Git blob identities. Untracked symbolic links are represented as link entries without following their targets.
|
|
48
51
|
- `getPathDiff(directory, { path, staged, contextLines })`: `getDiff` for one path, returning `{ diff, submodule }`. `submodule` is `null` for ordinary paths. For a gitlink it is `{ headCommit, indexCommit, worktreeCommit, hasTrackedChanges, hasUntrackedFiles, hasConflict }`, because a submodule that only gained untracked files shows as modified in status while its patch is empty. `worktreeCommit` is `null` when the submodule is not checked out. An unmerged gitlink has no single index commit, so it reports `hasConflict: true` with `indexCommit: null`. Exposed as `GET /api/git/diff`.
|
|
49
52
|
- Paths come from an earlier status listing and can stop resolving. Per-path operations reject with `error.code`: `path_not_found` when the path is absent from the working tree, index, and HEAD (for example, a file removed after the listing), `nested_repository` when the path is a directory holding its own `.git` that is not a submodule (status lists it as `dir/`), and `untracked_directory` when the path is a directory status kept as one `dir/` entry because it holds more untracked files than the expansion bound. `GET /api/git/diff` and `GET /api/git/file-diff` answer these with 404, 422 and 422 and a `{ error, code }` body instead of 500. Entry existence is read from `ls-files --stage` and `ls-tree` modes, not `cat-file -e`: a gitlink's commit is not in the parent's object store, and simple-git reports that silent exit 1 as success.
|
|
@@ -73,7 +76,7 @@ An identity is one repository's complete answer: signature, provider account (`a
|
|
|
73
76
|
- `getRemotes(directory)`: Get list of configured remotes.
|
|
74
77
|
|
|
75
78
|
### Worktree Operations
|
|
76
|
-
- `getWorktrees(directory)`: List all git worktrees for a repository. A directory outside any repository (or one that does not exist) is an authoritative empty list; any other git failure throws so callers keep their last known topology instead of clearing it. `GET /api/git/worktrees` answers such a failure with 500.
|
|
79
|
+
- `getWorktrees(directory)`: List all git worktrees for a repository. A directory outside any repository (or one that does not exist) is an authoritative empty list; any other git failure throws so callers keep their last known topology instead of clearing it. `GET /api/git/worktrees` answers such a failure with 500. A bare repository has no `--show-toplevel` answer, so the listing runs from the directory itself; the bare entry git lists for the repository's own directory is not a checkout and is left out of the result.
|
|
77
80
|
- `observeWorktreeTopology(directory)`: Compare the repository's registered linked-worktree set with the last one seen for it and notify `subscribeWorktreeTopologyChanges` listeners when it changed. The set is fingerprinted from the `worktrees` directory under the common Git directory (mtime plus entry names), so the check is a stat and a readdir; the common directory is resolved with `git rev-parse --git-common-dir` once per requested directory and cached. The first observation only records a baseline. Never throws.
|
|
78
81
|
- `subscribeWorktreeTopologyChanges(listener)`: Listener receives `{ directories, at }`, where `directories` are every directory of that repository the server has observed, so clients can map them onto registered projects. Returns an unsubscribe function.
|
|
79
82
|
- `validateWorktreeCreate(directory, input)`: Validate worktree creation parameters (mode, branchName, startRef, upstream config).
|
|
@@ -165,6 +168,15 @@ creation attempts cannot overwrite each other's rollback. If upstream fetch fail
|
|
|
165
168
|
bootstrap, tracking is left unset rather than writing `branch.*.remote` /
|
|
166
169
|
`branch.*.merge` for a ref that was never fetched. Contributor-fork creation always leaves tracking unset.
|
|
167
170
|
|
|
171
|
+
The agent tool's `session.create` with `pullRequest: <number>` makes the same
|
|
172
|
+
worktree without a client: `feature-routes-runtime.js` builds the change-request
|
|
173
|
+
source on the server (`source-control/change-request-worktree.js`) and hands it
|
|
174
|
+
to the same `worktree-creation.js` pipeline, so the remote, provenance, missing
|
|
175
|
+
upstream and Publish destination chooser are the dialog's. VS Code has no
|
|
176
|
+
contributor worktrees and no agent tool, so the parameter does not exist there;
|
|
177
|
+
a server without the pipeline answers 501 and never falls back to a plain
|
|
178
|
+
branch worktree.
|
|
179
|
+
|
|
168
180
|
### Commit and Remote Operations
|
|
169
181
|
- `commit(directory, message, options)`: Create a commit from the current index. An applied identity writes the repository's own `user.name` and `user.email`, and those decide. A repository on the System identity has none on purpose — it says no override applies — so the machine's own author answers, the way Git itself resolves it, and the panel names that author before the commit. With no author anywhere the commit is refused with the two ways to set one. `options.stageFiles` may be provided with `options.files` by older callers to stage only selected unstaged rows before committing, but the shared Git panel now stages/unstages explicitly before commit.
|
|
170
182
|
- `removeRemote(directory, options)`: Remove a configured remote (except `origin`).
|
|
@@ -183,7 +195,7 @@ Public snapshots contain `operationId`, `runtimeIdentity`, transport verificatio
|
|
|
183
195
|
|
|
184
196
|
### Exact-ref transport authority
|
|
185
197
|
|
|
186
|
-
Push, fetch, pull, remote-branch deletion, and sync accept only full refs. Sources are `refs/heads/*` or `refs/tags/*`; push destinations are heads or tags, deletion destinations are heads, fetch destinations are remote refs, heads, or tags, and pull destinations are heads. Sync narrows its fetch destination to `refs/remotes/*`, names its checked-out pull destination, and requires that destination to equal the exact push source. Planning accepts authoritative HTTPS, `ssh://`, and strict SCP endpoints only.
|
|
198
|
+
Push, fetch, pull, remote-branch deletion, and sync accept only full refs. Sources are `refs/heads/*` or `refs/tags/*`; push destinations are heads or tags, deletion destinations are heads, fetch destinations are remote refs, heads, or tags, and pull destinations are heads. Sync narrows its fetch destination to `refs/remotes/*`, names its checked-out pull destination, and requires that destination to equal the exact push source. Planning accepts authoritative HTTPS, `ssh://`, and strict SCP endpoints only, under the same endpoint rule as Clone. An `ssh://` endpoint may name its login user; HTTPS userinfo, any password, and query or fragment data fail before plan registration or Git argv construction. The SSH user stays in the internal raw endpoint: display URLs and fingerprints drop it, and URL display endpoints with userinfo fail. Planning validates the repository ID, binding and config revisions, remote, redacted endpoint fingerprint and display URL, transport mode, and opaque credential reference against source-control authority before resolving refs. Execution repeats authority validation before any subprocess or credential resolution and again immediately before each transfer Git process starts. It also compares a server-internal transport revision derived from effective system, global, local, worktree, and included Git configuration. The revision covers credential helpers and URL-scoped credential settings, SSH commands, URL rewrites, HTTP settings, and remote proxies. Values and origin paths never enter public plans or errors.
|
|
187
199
|
|
|
188
200
|
Push resolves and stores the source commit SHA while planning, rechecks it during both execution authority passes, and sends exactly `<pinned-sha>:<destination-ref>`. New-branch publication can request local upstream configuration; only after push success does the executor write the exact branch remote and merge ref, and a local configuration failure returns `partial`. An optional force lease is exactly `--force-with-lease=<destination-ref>:<expected-remote-sha>`. Remote deletion sends exactly `:<destination-ref>` and has the same uncertain-outcome rules as push. Fetch sends exactly `<source-ref>:<destination-ref>` with `--no-tags`. Pull pins the checked-out full HEAD ref and its commit SHA during planning, rechecks both before authentication, before fetch, and before merge, fetches to `refs/openchamber/network/<operationId>`, merges the fetched SHA with `--no-edit --no-verify`, then deletes the temporary ref. Remote names select authority; Git receives only the server-resolved raw endpoint.
|
|
189
201
|
|
|
@@ -247,9 +259,9 @@ The operation executor reads `.gitmodules` from the selected commit with `git co
|
|
|
247
259
|
|
|
248
260
|
Each submodule endpoint needs an exact auxiliary grant, including same-host paths. Cross-host children never receive the parent credential. The executor resolves every credential by the child endpoint's own opaque ID, checks the discovered endpoint against the grant before credential resolution and again before spawn, and redacts raw endpoints, paths, credentials, and process errors. One child failure does not erase completed siblings. Cancellation between children prevents later children from starting. Every existing component of a submodule checkout path must be a real directory; symbolic links fail before auxiliary authorization or transfer. After authorization, the executor checks the path components again and creates missing parent directories one level at a time before it creates the child checkout. Repair accepts an already-present child only when its HEAD exactly equals the committed gitlink, then inspects it recursively without another clone. A child directory created by the current failed transfer is quarantined and removed only when its filesystem identity still matches the operation's record.
|
|
249
261
|
|
|
250
|
-
LFS discovery combines effective attributes, bounded object batches, committed `.lfsconfig`, effective `lfs.url` and remote LFS URLs (Git configuration wins over `.lfsconfig`, and `lfs.url` over `remote.<name>.lfsurl`, as in Git LFS), and executable filter and transfer configuration. It queries attributes and object metadata in batches of at most
|
|
262
|
+
LFS discovery combines effective attributes, bounded object batches, committed `.lfsconfig`, effective `lfs.url` and remote LFS URLs (Git configuration wins over `.lfsconfig`, and `lfs.url` over `remote.<name>.lfsurl`, as in Git LFS), and executable filter and transfer configuration. It queries attributes and object metadata in batches of at most 1,024 paths, then reads only pointer-sized blobs through `cat-file --batch`. A complete ordinary repository is not rejected merely for exceeding 256 or 1,024 files, LFS files included: every pointer-sized blob is validated, but the scan keeps at most 256 attribute records and 256 pointer samples as evidence, so memory does not grow with the number of LFS files. `discoverLfs` therefore returns `attributePaths` and `pointers` as bounded samples; hydration reads only whether LFS is needed. Listing, query-output, and pointer-byte limits remain enforced. A checkout that exceeds one of them, such as a tracked-file listing over 16 MiB, fails hydration with `CHECKOUT_TOO_LARGE` under the `invalid` status, never as a configuration error. Submodule discovery limits keep `INVALID_REQUEST`. Incomplete or malformed discovery fails closed, never as `not-needed`. Custom transfer agents and non-canonical executable LFS filters fail before LFS execution. The effective HTTPS LFS endpoint gets its own exact auxiliary grant, including for an SSH Git checkout. Hydration runs explicit `git lfs fetch <selected-parent-remote> HEAD` and `git lfs checkout` under registry process ownership. A required missing client returns `GIT_LFS_CLIENT_MISSING` with `client-missing` instead of a generic clone failure.
|
|
251
263
|
|
|
252
|
-
Local worktree hydration first inspects local content. Git-route and OpenChamber-session creation both use the server's one durable bootstrap store and always invoke this inspection, including for repositories without a source-control binding. A checkout needing neither submodule nor LFS transfer requires no parent remote; required hydration without an exact source persists path-specific authorization data rather than choosing `origin` or reporting setup ready. Its operation-private durable marker carries no fabricated repository or remote authority and cannot complete a retained-checkout repair blocker. Managed pull and sync integration use a credential-free local execution context with network protocols, lazy fetching, hooks, content filters, fsmonitor, autostash, and recursive submodule operations disabled. Required post-integration hydration uses explicit grants; failure preserves the completed local update and skips sync publication.
|
|
264
|
+
Local worktree hydration first inspects local content. Git-route and OpenChamber-session creation both use the server's one durable bootstrap store and always invoke this inspection, including for repositories without a source-control binding. Both pass the repository authority from the binding read, so an unbound repository resolves its checkout remote and submodule endpoints as System grants against the store's tombstone revision, the same way it pushes and fetches. A checkout needing neither submodule nor LFS transfer requires no parent remote; required hydration without an exact source persists path-specific authorization data rather than choosing `origin` or reporting setup ready. Its operation-private durable marker carries no fabricated repository or remote authority and cannot complete a retained-checkout repair blocker. Managed pull and sync integration use a credential-free local execution context with network protocols, lazy fetching, hooks, content filters, fsmonitor, autostash, and recursive submodule operations disabled. Required post-integration hydration uses explicit grants; failure preserves the completed local update and skips sync publication.
|
|
253
265
|
|
|
254
266
|
### Managed LFS publication
|
|
255
267
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -6,10 +7,6 @@ import { withSourceControlFileLock } from '../source-control/file-lock.js';
|
|
|
6
7
|
|
|
7
8
|
const VERSION = 1;
|
|
8
9
|
const DEFAULT_MAX_RECORDS = 512;
|
|
9
|
-
const isPlainObject = (value) => value === Object(value)
|
|
10
|
-
&& !Array.isArray(value)
|
|
11
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
12
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
13
10
|
const isIdentifier = (value) => isString(value) && value.length > 0 && value.length <= 512
|
|
14
11
|
&& value.trim() === value && !/[\0\r\n]/.test(value);
|
|
15
12
|
const exactKeys = (value, keys) => {
|
|
@@ -163,10 +160,16 @@ export function createContributorProvenanceStore({
|
|
|
163
160
|
return enqueue(async () => {
|
|
164
161
|
const [state, identities] = await Promise.all([
|
|
165
162
|
readState(),
|
|
166
|
-
|
|
163
|
+
// A listing entry whose git directory no longer resolves (a worktree
|
|
164
|
+
// deleted outside git, a bare repository root, a broken .git link)
|
|
165
|
+
// has no identity to look up. It reads as no provenance rather than
|
|
166
|
+
// failing the whole listing; action-time reads stay strict.
|
|
167
|
+
Promise.all(directories.map((directory) => resolveIdentity(directory).catch(() => null))),
|
|
167
168
|
]);
|
|
168
169
|
const recordsByWorktree = new Map(state.records.map((record) => [record.worktreeId, record]));
|
|
169
|
-
return identities.map((identity) =>
|
|
170
|
+
return identities.map((identity) => (identity
|
|
171
|
+
? recordForIdentity(recordsByWorktree.get(identity.worktreeId), identity)
|
|
172
|
+
: { revision: 0, provenance: null }));
|
|
170
173
|
});
|
|
171
174
|
};
|
|
172
175
|
const compareAndSwap = (directory, expectedRevision, provenance) => {
|
|
@@ -130,4 +130,33 @@ describe('contributor provenance storage', () => {
|
|
|
130
130
|
expect(records.every((record) => record.provenance === null)).toBe(true);
|
|
131
131
|
expect(storeReads).toBe(1);
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it('reads an unresolvable listed worktree as no provenance instead of failing the batch', async () => {
|
|
135
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'openchamber-contributor-batch-'));
|
|
136
|
+
roots.push(root);
|
|
137
|
+
const gitDirectory = path.join(root, 'one');
|
|
138
|
+
await fs.mkdir(gitDirectory);
|
|
139
|
+
const store = createContributorProvenanceStore({
|
|
140
|
+
filePath: path.join(root, 'provenance.json'),
|
|
141
|
+
resolveRepositoryIdentity: async (directory) => (directory === '/gone'
|
|
142
|
+
? { supported: false }
|
|
143
|
+
: { supported: true, repositoryId: 'repo_one' }),
|
|
144
|
+
resolveGitPaths: async (directory) => (directory === '/gone'
|
|
145
|
+
? { supported: false, reason: 'not-a-git-repository' }
|
|
146
|
+
: { supported: true, bare: false, gitDirectory }),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// A worktree deleted outside git (or a bare repository root) has no
|
|
150
|
+
// identity to look up; the rest of the listing still answers.
|
|
151
|
+
const records = await store.readMany(['/one', '/gone']);
|
|
152
|
+
expect(records).toHaveLength(2);
|
|
153
|
+
expect(records[0].provenance).toBe(null);
|
|
154
|
+
expect(records[1]).toEqual({ revision: 0, provenance: null });
|
|
155
|
+
|
|
156
|
+
// Store corruption stays a hard failure.
|
|
157
|
+
await fs.writeFile(path.join(root, 'provenance.json'), '{"version":1,"records":"bad"}', { mode: 0o600 });
|
|
158
|
+
await expect(store.readMany(['/one', '/gone'])).rejects.toMatchObject({
|
|
159
|
+
code: 'CONTRIBUTOR_PROVENANCE_STORE_INVALID',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
133
162
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
3
|
import http from 'node:http';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -11,7 +12,6 @@ const MAX_BODY_BYTES = 64 * 1024;
|
|
|
11
12
|
const HELPER_PATH = fileURLToPath(new URL('./credential-helper.js', import.meta.url));
|
|
12
13
|
/** Where the helper finds its lease nonce; set on the Git process, inherited by the helper. */
|
|
13
14
|
export const GIT_CREDENTIAL_NONCE_ENV = 'OPENCHAMBER_GIT_CREDENTIAL_NONCE';
|
|
14
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Git's credential wire format: `key=value` lines, one per attribute. Exported
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -10,10 +11,6 @@ const VERSIONED_REFERENCE_PREFIX = 'ocgit:v2';
|
|
|
10
11
|
const DEFAULT_PORTS = Object.freeze({ http: 80, https: 443, ssh: 22 });
|
|
11
12
|
const MAX_SSH_PRIVATE_KEY_BYTES = 128 * 1024;
|
|
12
13
|
const SSH_PRIVATE_KEY_MARKER = /-----BEGIN (?:OPENSSH |RSA |DSA |EC |ENCRYPTED )?PRIVATE KEY-----/;
|
|
13
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
14
|
-
const isPlainObject = (value) => value === Object(value)
|
|
15
|
-
&& !Array.isArray(value)
|
|
16
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
17
14
|
|
|
18
15
|
const requiredString = (value, name) => {
|
|
19
16
|
if (!isString(value) || !value || value.trim() !== value || /[\0\r\n]/.test(value)) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
const CONTROL_PATTERN = /[\0-\x20\x7f]/;
|
|
4
5
|
const SCP_PATTERN = /^(?:([^@/:\s]+)@)?([A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?):([^\\]+)$/;
|
|
5
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
6
6
|
|
|
7
7
|
const discoveryError = (message) => Object.assign(new Error(message), {
|
|
8
8
|
code: 'INVALID_GIT_DISCOVERY_ENDPOINT',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
2
|
+
|
|
2
3
|
const text = (value) => (isString(value) ? value.trim() : '');
|
|
3
4
|
|
|
4
5
|
const sameAccount = (left, right) => Boolean(left) && Boolean(right)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -6,10 +7,6 @@ import { withSourceControlFileLockSync } from '../source-control/file-lock.js';
|
|
|
6
7
|
|
|
7
8
|
const FILE_NAME = 'git-identities.json';
|
|
8
9
|
const legacyDataDir = () => path.join(os.homedir(), '.config', 'openchamber');
|
|
9
|
-
const isPlainObject = (value) => value === Object(value)
|
|
10
|
-
&& !Array.isArray(value)
|
|
11
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
12
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
13
10
|
const invalidStore = (cause) => Object.assign(new Error('Git identity profile store is invalid', { cause }), {
|
|
14
11
|
code: 'GIT_IDENTITY_STORE_INVALID',
|
|
15
12
|
});
|
|
@@ -270,4 +267,6 @@ export const getProfiles = () => defaultStore().getProfiles();
|
|
|
270
267
|
export const getProfile = (id) => defaultStore().getProfile(id);
|
|
271
268
|
export const createProfile = (profileData) => defaultStore().createProfile(profileData);
|
|
272
269
|
export const updateProfile = (id, updates) => defaultStore().updateProfile(id, updates);
|
|
270
|
+
|
|
271
|
+
/** @public Read by Git routes through the lazy-loaded Git library. */
|
|
273
272
|
export const deleteProfile = (id) => defaultStore().deleteProfile(id);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
1
2
|
import { normalizeDiscoveryEndpoint, resolveGitRelativeEndpoint } from './discovery-endpoint.js';
|
|
2
3
|
|
|
3
4
|
export const LFS_DISCOVERY_LIMITS = Object.freeze({
|
|
@@ -25,7 +26,6 @@ const SAFE_FILTERS = Object.freeze({
|
|
|
25
26
|
'filter.lfs.smudge': 'git-lfs smudge -- %f',
|
|
26
27
|
'filter.lfs.process': 'git-lfs filter-process',
|
|
27
28
|
});
|
|
28
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
29
29
|
|
|
30
30
|
const lfsError = (message, code = 'INVALID_LFS_DISCOVERY_INPUT') => Object.assign(new Error(message), { code });
|
|
31
31
|
|
|
@@ -80,9 +80,6 @@ const parseAttributes = (value, limits) => {
|
|
|
80
80
|
if (fields[index + 2] === 'lfs' && !seen.has(filePath)) {
|
|
81
81
|
seen.add(filePath);
|
|
82
82
|
paths.push(filePath);
|
|
83
|
-
if (paths.length > limits.maxPublicRecords) {
|
|
84
|
-
throw lfsError('LFS attribute records exceed their public record limit', 'LFS_DISCOVERY_LIMIT_EXCEEDED');
|
|
85
|
-
}
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
return paths;
|
|
@@ -144,6 +141,14 @@ export function parseTreeObjects(treeOutput) {
|
|
|
144
141
|
return entries;
|
|
145
142
|
}
|
|
146
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Every tracked file is checked, and every pointer-sized blob that claims to be
|
|
146
|
+
* an LFS pointer is validated. The result keeps only bounded evidence: up to
|
|
147
|
+
* `maxPublicRecords` attribute records and `maxPointerSamples` pointer samples.
|
|
148
|
+
* Hydration needs to know whether LFS is in use, not an inventory, so a
|
|
149
|
+
* repository with thousands of LFS files is scanned in the same memory as one
|
|
150
|
+
* with a handful.
|
|
151
|
+
*/
|
|
147
152
|
export async function scanLfsFiles(filesOutput, query, gitlinkPaths = [], treeObjects = null) {
|
|
148
153
|
const limits = LFS_DISCOVERY_LIMITS;
|
|
149
154
|
const text = boundedText(filesOutput, limits.maxFilesBytes, 'Tracked files output');
|
|
@@ -176,15 +181,11 @@ export async function scanLfsFiles(filesOutput, query, gitlinkPaths = [], treeOb
|
|
|
176
181
|
}
|
|
177
182
|
for (const file of selected) {
|
|
178
183
|
const record = `${file}\0filter\0lfs\0`;
|
|
179
|
-
|
|
180
|
-
if (attributesBytes > limits.maxAttributesBytes)
|
|
181
|
-
|
|
182
|
-
}
|
|
184
|
+
const recordBytes = Buffer.byteLength(record);
|
|
185
|
+
if (attributes.length >= limits.maxPublicRecords || attributesBytes + recordBytes > limits.maxAttributesBytes) break;
|
|
186
|
+
attributesBytes += recordBytes;
|
|
183
187
|
attributes.push(record);
|
|
184
188
|
}
|
|
185
|
-
if (attributes.length > limits.maxPublicRecords) {
|
|
186
|
-
throw lfsError('LFS attribute records exceed their public record limit', 'LFS_DISCOVERY_LIMIT_EXCEEDED');
|
|
187
|
-
}
|
|
188
189
|
|
|
189
190
|
// Query sizes without reading blobs. Only pointer-sized blobs enter the
|
|
190
191
|
// bounded content query; Git LFS pointers fit within 1 KiB.
|
|
@@ -219,9 +220,8 @@ export async function scanLfsFiles(filesOutput, query, gitlinkPaths = [], treeOb
|
|
|
219
220
|
for (const { candidate, content } of await readSmallBlobs(query, candidates, limits, 'Git object')) {
|
|
220
221
|
if (content.subarray(0, prefix.length).equals(prefix)) {
|
|
221
222
|
parseLfsPointer(content);
|
|
222
|
-
pointerSamples.
|
|
223
|
-
|
|
224
|
-
throw lfsError('LFS pointers exceed their record limit', 'LFS_DISCOVERY_LIMIT_EXCEEDED');
|
|
223
|
+
if (pointerSamples.length < limits.maxPointerSamples) {
|
|
224
|
+
pointerSamples.push({ path: candidate.path, content: Buffer.from(content) });
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
}
|
|
@@ -190,12 +190,22 @@ describe('incremental LFS file discovery', () => {
|
|
|
190
190
|
await expect(fixture.run()).rejects.toThrow();
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
it('
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
it.each([257, 3_000])('accepts %i LFS files with long paths and keeps bounded evidence', async (count) => {
|
|
194
|
+
const directory = `assets/${'deeply-nested-directory/'.repeat(8)}`;
|
|
195
|
+
const files = Array.from({ length: count }, (_, index) => ({ path: `${directory}file-${index}.bin`, content: pointer() }));
|
|
196
|
+
const fixture = scanFixture(files, { attribute: () => 'lfs' });
|
|
197
|
+
const scan = await fixture.run();
|
|
198
|
+
expect(scan.pointerSamples).toHaveLength(LFS_DISCOVERY_LIMITS.maxPointerSamples);
|
|
199
|
+
expect(Buffer.byteLength(scan.attributesOutput)).toBeLessThanOrEqual(LFS_DISCOVERY_LIMITS.maxAttributesBytes);
|
|
200
|
+
const result = discoverLfs(input(scan));
|
|
201
|
+
expect(result.needed).toBe(true);
|
|
202
|
+
expect(result.attributePaths).toHaveLength(LFS_DISCOVERY_LIMITS.maxPublicRecords);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('validates every pointer, including those past the kept samples', async () => {
|
|
206
|
+
const files = Array.from({ length: 1_500 }, (_, index) => ({ path: `file-${index}`, content: pointer() }));
|
|
207
|
+
files.push({ path: 'late-broken', content: pointer().replace('size 42', 'size forty-two') });
|
|
208
|
+
await expect(scanFixture(files).run()).rejects.toMatchObject({ code: 'INVALID_LFS_DISCOVERY_INPUT' });
|
|
199
209
|
});
|
|
200
210
|
|
|
201
211
|
it('does not query unsafe or incomplete paths and enforces the listing byte bound', async () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -8,10 +9,6 @@ import { createHttpsCredentialReference, normalizeGitRemoteEndpoint } from './cr
|
|
|
8
9
|
const OPERATIONS = ['push', 'fetch', 'pull', 'delete-remote-branch', 'sync', 'clone', 'checkout-hydration'];
|
|
9
10
|
const SHA_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i;
|
|
10
11
|
const REF_COMPONENT_PATTERN = /^(?!\.)(?!.*(?:\.\.|\/\.|\.lock(?:\/|$)))(?!.*[~^:?*[\\\s])(?!.*\/$)(?!.*\/\/)[^\0-\x20\x7f]+$/;
|
|
11
|
-
const isPlainObject = (value) => value === Object(value)
|
|
12
|
-
&& !Array.isArray(value)
|
|
13
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
14
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
15
12
|
const isBoolean = (value) => Object.prototype.toString.call(value) === '[object Boolean]'
|
|
16
13
|
&& (value === true || value === false);
|
|
17
14
|
const hasExactKeys = (value, required, optional = []) => {
|
|
@@ -39,13 +36,24 @@ const requiredString = (value, name) => {
|
|
|
39
36
|
if (result.startsWith('-') || /[\0\r\n]/.test(result)) throw planError(`${name} is invalid`);
|
|
40
37
|
return result;
|
|
41
38
|
};
|
|
39
|
+
// A server-generated digest the client echoes back (configRevision, endpoint
|
|
40
|
+
// fingerprint, transportRevision): an opaque sha256/base64url value, never a CLI
|
|
41
|
+
// argument. Base64url may legitimately start with '-', so the leading-dash
|
|
42
|
+
// injection guard in `requiredString` must not apply; only real control
|
|
43
|
+
// characters are rejected.
|
|
44
|
+
const parseDigest = (value, name) => {
|
|
45
|
+
if (!isString(value) || !value.trim()) throw planError(`${name} is required`);
|
|
46
|
+
const result = value.trim();
|
|
47
|
+
if (/[\0\r\n]/.test(result)) throw planError(`${name} is invalid`);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
42
50
|
const parseSha = (value, name) => {
|
|
43
51
|
const sha = requiredString(value, name);
|
|
44
52
|
if (!SHA_PATTERN.test(sha)) throw planError(`${name} is invalid`);
|
|
45
53
|
return sha.toLowerCase();
|
|
46
54
|
};
|
|
47
55
|
const parseFingerprint = (value, name) => {
|
|
48
|
-
const fingerprint =
|
|
56
|
+
const fingerprint = parseDigest(value, name);
|
|
49
57
|
if (!/^[A-Za-z0-9_-]{43}$/.test(fingerprint)) throw planError(`${name} is invalid`);
|
|
50
58
|
return fingerprint;
|
|
51
59
|
};
|
|
@@ -60,7 +68,7 @@ const parseRemote = (value) => {
|
|
|
60
68
|
name: requiredString(value.name, 'remote name'),
|
|
61
69
|
endpoint: {
|
|
62
70
|
displayUrl: requiredString(value.endpoint.displayUrl, 'remote displayUrl'),
|
|
63
|
-
fingerprint:
|
|
71
|
+
fingerprint: parseDigest(value.endpoint.fingerprint, 'remote fingerprint'),
|
|
64
72
|
},
|
|
65
73
|
};
|
|
66
74
|
};
|
|
@@ -79,7 +87,7 @@ const parseHydrationRequirements = (value) => {
|
|
|
79
87
|
|| !hasExactKeys(entry.endpoint, ['displayUrl', 'fingerprint'])) {
|
|
80
88
|
throw planError('checkout hydration requirement is invalid');
|
|
81
89
|
}
|
|
82
|
-
|
|
90
|
+
assertSafeDisplayEndpoint(entry.endpoint.displayUrl);
|
|
83
91
|
return {
|
|
84
92
|
kind: entry.kind,
|
|
85
93
|
path: parseCheckoutPath(entry.path),
|
|
@@ -158,7 +166,7 @@ const parseAuxiliaryGrants = (value) => {
|
|
|
158
166
|
throw planError('auxiliary grant is invalid');
|
|
159
167
|
}
|
|
160
168
|
const transportMode = parseTransportMode(grant.transportMode);
|
|
161
|
-
|
|
169
|
+
assertSafeDisplayEndpoint(grant.endpoint.displayUrl);
|
|
162
170
|
const parsed = {
|
|
163
171
|
kind: grant.kind,
|
|
164
172
|
endpoint: {
|
|
@@ -184,33 +192,41 @@ const parseAuxiliaryGrants = (value) => {
|
|
|
184
192
|
if (new Set(keys).size !== keys.length) throw planError('auxiliary grants must be unique');
|
|
185
193
|
return grants;
|
|
186
194
|
};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Whether Git may receive this endpoint: HTTPS, `ssh://`, or strict SCP.
|
|
197
|
+
* An SSH URL may name its login user, as SCP does; HTTPS userinfo and any
|
|
198
|
+
* password are credentials and never pass. Clone and every existing-remote
|
|
199
|
+
* transfer share this rule.
|
|
200
|
+
*/
|
|
201
|
+
const isSafeTransferEndpoint = (value) => {
|
|
202
|
+
if (value.length > 4096) return false;
|
|
190
203
|
if (!value.includes('://')) {
|
|
191
204
|
const match = value.match(/^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?:([^\s:\\]+)$/);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
throw planError('Git endpoint is invalid');
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
205
|
+
return Boolean(match) && !/[?#]/.test(value) && !match[1].startsWith('-')
|
|
206
|
+
&& match[1].split('/').every((part) => part && part !== '.' && part !== '..');
|
|
197
207
|
}
|
|
198
208
|
let parsed;
|
|
199
|
-
try {
|
|
200
|
-
parsed = new URL(value);
|
|
201
|
-
} catch {
|
|
202
|
-
throw planError('Git endpoint is invalid');
|
|
203
|
-
}
|
|
204
209
|
let pathname;
|
|
205
210
|
try {
|
|
211
|
+
parsed = new URL(value);
|
|
206
212
|
pathname = decodeURIComponent(parsed.pathname);
|
|
207
213
|
} catch {
|
|
208
|
-
|
|
214
|
+
return false;
|
|
209
215
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
return ['https:', 'ssh:'].includes(parsed.protocol)
|
|
217
|
+
&& (!parsed.username || (parsed.protocol === 'ssh:' && /^[A-Za-z0-9._][A-Za-z0-9._-]*$/.test(parsed.username)))
|
|
218
|
+
&& !parsed.password && !parsed.search && !parsed.hash
|
|
219
|
+
&& Boolean(parsed.hostname) && Boolean(parsed.pathname) && parsed.pathname !== '/'
|
|
220
|
+
&& !/[\0-\x20\x7f\\]/.test(pathname)
|
|
221
|
+
&& pathname.split('/').slice(1).every((part) => part && part !== '.' && part !== '..');
|
|
222
|
+
};
|
|
223
|
+
const assertSafeExistingEndpoint = (endpoint) => {
|
|
224
|
+
if (!isSafeTransferEndpoint(requiredString(endpoint, 'Git endpoint'))) throw planError('Git endpoint is invalid');
|
|
225
|
+
};
|
|
226
|
+
// Display URLs are redacted, so a URL-form display never carries userinfo.
|
|
227
|
+
const assertSafeDisplayEndpoint = (endpoint) => {
|
|
228
|
+
const value = requiredString(endpoint, 'Git endpoint');
|
|
229
|
+
if (!isSafeTransferEndpoint(value) || (value.includes('://') && new URL(value).username)) {
|
|
214
230
|
throw planError('Git endpoint is invalid');
|
|
215
231
|
}
|
|
216
232
|
};
|
|
@@ -235,35 +251,7 @@ const parseRemoteFetchMapping = (remoteName, output) => {
|
|
|
235
251
|
};
|
|
236
252
|
const parseCloneEndpoint = (value) => {
|
|
237
253
|
const endpoint = requiredString(value, 'remoteUrl');
|
|
238
|
-
if (!endpoint
|
|
239
|
-
&& /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?:[^\s:\\]+$/.test(endpoint)) {
|
|
240
|
-
const separator = endpoint.indexOf(':');
|
|
241
|
-
const repositoryPath = endpoint.slice(separator + 1);
|
|
242
|
-
if (/[?#]/.test(endpoint) || repositoryPath.startsWith('-')
|
|
243
|
-
|| repositoryPath.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
244
|
-
throw planError('remoteUrl is invalid');
|
|
245
|
-
}
|
|
246
|
-
return endpoint;
|
|
247
|
-
}
|
|
248
|
-
let parsed;
|
|
249
|
-
try {
|
|
250
|
-
parsed = new URL(endpoint);
|
|
251
|
-
} catch {
|
|
252
|
-
throw planError('remoteUrl is invalid');
|
|
253
|
-
}
|
|
254
|
-
let pathname;
|
|
255
|
-
try {
|
|
256
|
-
pathname = decodeURIComponent(parsed.pathname);
|
|
257
|
-
} catch {
|
|
258
|
-
throw planError('remoteUrl is invalid');
|
|
259
|
-
}
|
|
260
|
-
if (!['https:', 'ssh:'].includes(parsed.protocol) || parsed.password || parsed.search || parsed.hash
|
|
261
|
-
|| !parsed.hostname || !parsed.pathname || parsed.pathname === '/'
|
|
262
|
-
|| (parsed.protocol === 'https:' && parsed.username)
|
|
263
|
-
|| /[\0-\x20\x7f\\]/.test(pathname)
|
|
264
|
-
|| pathname.split('/').slice(1).some((part) => !part || part === '.' || part === '..')) {
|
|
265
|
-
throw planError('remoteUrl is invalid');
|
|
266
|
-
}
|
|
254
|
+
if (!isSafeTransferEndpoint(endpoint)) throw planError('remoteUrl is invalid');
|
|
267
255
|
return endpoint;
|
|
268
256
|
};
|
|
269
257
|
/**
|
|
@@ -376,7 +364,7 @@ export function createNetworkOperationPlanner({
|
|
|
376
364
|
throw contributorError('DESTINATION_SELECTION_REQUIRED', 'Contributor push destination selection is required');
|
|
377
365
|
}
|
|
378
366
|
const repositoryId = requiredString(input.repositoryId, 'repositoryId');
|
|
379
|
-
const configRevision =
|
|
367
|
+
const configRevision = parseDigest(input.configRevision, 'configRevision');
|
|
380
368
|
if (!Number.isSafeInteger(input.bindingRevision) || input.bindingRevision < 0) {
|
|
381
369
|
throw planError('bindingRevision is required');
|
|
382
370
|
}
|
|
@@ -425,7 +413,7 @@ export function createNetworkOperationPlanner({
|
|
|
425
413
|
const fetchAuthority = await validateGitTransportContext(authorityInput(fetchRemote, 'fetch'));
|
|
426
414
|
const pushAuthority = await validateGitTransportContext(authorityInput(pushRemote, 'push'));
|
|
427
415
|
const validateAuthority = (authority, remote, transportMode) => {
|
|
428
|
-
const transportRevision =
|
|
416
|
+
const transportRevision = parseDigest(authority.transportRevision, 'transportRevision');
|
|
429
417
|
assertSafeExistingEndpoint(authority.endpoint);
|
|
430
418
|
if (transportMode === 'anonymous' && (authority.credentialId !== undefined
|
|
431
419
|
|| normalizeGitRemoteEndpoint(authority.endpoint).protocol !== 'https')) {
|
|
@@ -531,7 +519,7 @@ export function createNetworkOperationPlanner({
|
|
|
531
519
|
const directory = requiredString(input.directory, 'directory');
|
|
532
520
|
const contributor = await readContributor(directory);
|
|
533
521
|
const repositoryId = requiredString(input.repositoryId, 'repositoryId');
|
|
534
|
-
const configRevision =
|
|
522
|
+
const configRevision = parseDigest(input.configRevision, 'configRevision');
|
|
535
523
|
const remote = parseRemote(input.remote);
|
|
536
524
|
const sourceNamespaces = ['refs/heads/', 'refs/tags/'];
|
|
537
525
|
const destinationNamespaces = operation === 'fetch'
|
|
@@ -580,7 +568,7 @@ export function createNetworkOperationPlanner({
|
|
|
580
568
|
remote: remote.name,
|
|
581
569
|
endpointKind,
|
|
582
570
|
});
|
|
583
|
-
const transportRevision =
|
|
571
|
+
const transportRevision = parseDigest(authority.transportRevision, 'transportRevision');
|
|
584
572
|
assertSafeExistingEndpoint(authority.endpoint);
|
|
585
573
|
if (transportMode === 'anonymous' && (authority.credentialId !== undefined
|
|
586
574
|
|| normalizeGitRemoteEndpoint(authority.endpoint).protocol !== 'https')) {
|
|
@@ -783,7 +771,7 @@ export function createNetworkOperationPlanner({
|
|
|
783
771
|
}
|
|
784
772
|
const directory = requiredString(input.directory, 'directory');
|
|
785
773
|
const repositoryId = requiredString(input.repositoryId, 'repositoryId');
|
|
786
|
-
const configRevision =
|
|
774
|
+
const configRevision = parseDigest(input.configRevision, 'configRevision');
|
|
787
775
|
if (!Number.isSafeInteger(input.bindingRevision) || input.bindingRevision < 0) {
|
|
788
776
|
throw planError('bindingRevision is required');
|
|
789
777
|
}
|
|
@@ -831,7 +819,7 @@ export function createNetworkOperationPlanner({
|
|
|
831
819
|
directory,
|
|
832
820
|
endpointKind: 'fetch',
|
|
833
821
|
transportMode: authority.transportMode,
|
|
834
|
-
transportRevision:
|
|
822
|
+
transportRevision: parseDigest(authority.transportRevision, 'transportRevision'),
|
|
835
823
|
expectedHeadSha,
|
|
836
824
|
plannedRequirements: requirements,
|
|
837
825
|
plannedTransfers,
|
|
@@ -889,7 +877,7 @@ export function createNetworkOperationPlanner({
|
|
|
889
877
|
directory,
|
|
890
878
|
repositoryId: requiredString(input.repositoryId, 'repositoryId'),
|
|
891
879
|
bindingRevision: input.bindingRevision,
|
|
892
|
-
configRevision:
|
|
880
|
+
configRevision: parseDigest(input.configRevision, 'configRevision'),
|
|
893
881
|
remote: remote.name,
|
|
894
882
|
endpointKind: 'push',
|
|
895
883
|
});
|